Using RestTemplate in Spring Boot to connect to a server, I have this generic class to map responses from the server I'm connecting to:
@Data
public class ServicesResponseDTO<T> implements Serializable {
private String[] messages;
private String operation;
private String code;
private List<T> data;
private String start;
private String limit;
private String total;
private String timeInMillis;
private String message;
public boolean esOK () {
return "OK".equals(code);
}
public boolean esOK_NO_DATA () {
return "OK_NO_DATA".equals(code);
}
public boolean noOK_USER_NO_LOGADO () {
return "ERROR_NO_USER_LOGGED".equals(code);
}
}
and this generic class to parse every call:
public class CustomRestClient<T extends BasicDTO> {
private final Class<T> typeParameterClass;
private ServiceClientFactory clientFactory;
public CustomRestClient(Class<T> type, ServiceClientFactory clientFactory) {
typeParameterClass = type;
this.clientFactory = clientFactory;
}
private RestTemplate getRestTemplate() {
return clientFactory.getRestTemplate();
}
public T doGet(UriComponentsBuilder builder) throws ServiceException {
RestTemplate restTemplate = getRestTemplate();
T responseEntity = null;
String responseCode = null;
try {
ResponseEntity<String> response = restTemplate.getForEntity(builder.toUriString(), String.class);
ObjectMapper mapper = JsonMapper.builder().configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true).build();
TypeFactory t = TypeFactory.defaultInstance();
ServicesResponseDTO<T> respuesta = mapper.readValue(response.getBody(), t.constructParametricType(ServicesResponseDTO.class, typeParameterClass));
responseCode = respuesta.getCode();
if (respuesta.esOK_NO_DATA()) {
return null;
}
if (!respuesta.esOK()) {
log.warn(msgError);
throw new ServiceException(msgError);
}
responseEntity = respuesta.getData()!=null && !respuesta.getData().isEmpty() ? respuesta.getData().get(0) : null;
TripleDESCipher.decipher(responseEntity);
} catch (JsonProcessingException e) {
log.warn(msgError);
throw new ServiceException(msgError);
} catch (ResourceAccessException e) {
if (e.getCause() instanceof HttpHostConnectException) {
throw new ServiceException("Error connecting to the data origin");
}
if (e.getCause() instanceof SocketTimeoutException) {
throw new ServiceException("Timeout awaiting data origin");
}
throw new ServiceException("No controlled error with data origin");
} catch (Exception e) {
throw new ServiceException("No controlled error with data origin");
}
return responseEntity;
}
}
That is working.
But now RestTemplate is going to get deprecated and I'm migrating to RestClient, so did this:
public class CustomRestClient<T extends BasicDTO> {
private final Class<T> typeParameterClass;
private ServiceClientFactory clientFactory;
public CustomRestClient(Class<T> type, ServiceClientFactory clientFactory) {
typeParameterClass = type;
this.clientFactory = clientFactory;
}
private RestClient getRestClient() {
return clientFactory.getRestClient();
}
private ParameterizedTypeReference<ServicesResponseDTO<T>> getParameterizedTypeReference(Class<T> clazz) {
return new ParameterizedTypeReference<ServicesResponseDTO<T>>() {};
}
public T doGet(UriComponentsBuilder builder) throws ServiceException {
RestClient restClient = getRestClient();
T responseEntity = null;
String responseCode = null;
try {
ParameterizedTypeReference<ServicesResponseDTO<T>> tipo = getParameterizedTypeReference(typeParameterClass);
ResponseEntity<ServicesResponseDTO<T>> response = restClient.get()
.uri(builder.toUriString()).accept(MediaType.APPLICATION_JSON).retrieve().toEntity(tipo);
ServicesResponseDTO<T> respuesta = (ServicesResponseDTO<T>) response.getBody();
responseCode = respuesta.getCode();
if (respuesta.esOK_NO_DATA()) {
return null;
}
if (!respuesta.esOK()) {
log.warn(msgError);
throw new ServiceException(msgError);
}
responseEntity = respuesta.getData()!=null && !respuesta.getDaa().isEmpty() ? respuesta.getData().get(0) : null;
TripleDESCipher.decipher(responseEntity);
} catch (ResourceAccessException e) {
if (e.getCause() instanceof HttpHostConnectException) {
throw new ServiceException("Error connecting to the data origin");
}
if (e.getCause() instanceof SocketTimeoutException) {
log.warn("Timeout esperando la respuesta del endpoint. "+e.getLocalizedMessage());
throw new ServiceException("Timeout awaiting data origin");
}
throw new ServiceException("No controlled error with data origin");
} catch (Exception e) {
throw new ServiceException("No controlled error with data origin");
}
return responseEntity;
}
}
Basically, changed from RestTemplate to RestClient and the way to parse it.
The problem I'm facing is that ResponseEntity is getting parsed into
ResponseEntity<ServicesResponseDTO<BasicDTO>> instead of ResponseEntity<ServicesResponseDTO<ClubDTO>>, while ClubDTO is:
@Data
@EqualsAndHashCode(callSuper = true)
public class FenixClubDTO extends BasicDTO {
data
}
But if I change the doGet method to return a ClubDTO and:
ParameterizedTypeReference<ServicesResponseDTO<T>> tipo = getParameterizedTypeReference(typeParameterClass);
ResponseEntity<ServicesResponseDTO<T>> response = restClient.get().uri(builder.toUriString()).accept(MediaType.APPLICATION_JSON).retrieve().toEntity(tipo);
ServicesResponseDTO<T> respuesta = (ServicesResponseDTO<T>) response.getBody();
to:
ParameterizedTypeReference<ServicesResponseDTO<ClubDTO>> tipo = new ParameterizedTypeReference<ServicesResponseDTO<ClubDTO>>() {};
ResponseEntity<ServicesResponseDTO<ClubDTO>> response = restClient.get()
.uri(builder.toUriString()).accept(MediaType.APPLICATION_JSON).retrieve().toEntity(tipo);
ServicesResponseDTO<FenixClubDTO> respuesta = (ServicesResponseDTO<ClubDTO>) response.getBody();
then it gets a ServiceResponseDTO<ClubDTO> (expected) instead of ServiceResponseDTO<BasicDTO>.
So I guess the problem is with the ParameterizedTypeReference that's using a type T that's also extending.
With this workaround it works, but I really dislike it. It consists in creating every return type in the config and not only the client.
@Configuration
public class ApiClientConfiguration {
@Bean
public ParameterizedTypeReference<ServicesResponseDTO<ClubDTO>> clubParameterTypeReference() {
return new ParameterizedTypeReference<ServicesResponseDTO<ClubDTO>>() {};
}
}
change the notation of the doGet method to public T doGet(UriComponentsBuilder builder, ParameterizedTypeReference<ServicesResponseDTO<T>> responseType ) throws ServiceException {
and then call the doGet like this
ClubDTO respuesta = client.doGet(builder, type);
instead of cleaner and expected ClubDTO respuesta = client.doGet(builder); and creating the type inside the doGet (I definetely don't want the service to know ParameterizedTypeReference<ServicesResponseDTO<ClubDTO>>, that should be just a client thing)
How to solve it?