Pour utiliser les types génériques avec Spring RestTemplate, nous devons utiliser ParameterizedTypeReference
( Impossible d'obtenir un ResponseEntity générique <T>, où T est une classe générique "SomeClass <SomeGenericType>" )
Supposons que j'ai de la classe
public class MyClass {
int users[];
public int[] getUsers() { return users; }
public void setUsers(int[] users) {this.users = users;}
}
Et une classe de wrapper
public class ResponseWrapper <T> {
T response;
public T getResponse () { return response; }
public void setResponse(T response) {this.response = response;}
}
Donc, si j'essaie de faire quelque chose comme ça, tout va bien.
public ResponseWrapper<MyClass> makeRequest(URI uri) {
ResponseEntity<ResponseWrapper<MyClass>> response = template.exchange(
uri,
HttpMethod.POST,
null,
new ParameterizedTypeReference<ResponseWrapper<MyClass>>() {});
return response;
}
Mais quand j'essaie de créer une variante générique de la méthode ci-dessus ...
public <T> ResponseWrapper<T> makeRequest(URI uri, Class<T> clazz) {
ResponseEntity<ResponseWrapper<T>> response = template.exchange(
uri,
HttpMethod.POST,
null,
new ParameterizedTypeReference<ResponseWrapper<T>>() {});
return response;
}
... et en appelant cette méthode comme si ...
makeRequest(uri, MyClass.class)
... au lieu d'obtenir ResponseEntity<ResponseWrapper<MyClass>>
objet que je reçois ResponseEntity<ResponseWrapper<LinkedHashSet>>
objet.
Comment puis-je résoudre ce problème? Est-ce un bogue RestTemplate?
UPDATE 1 Grâce à @Sotirios, je comprends le concept. Malheureusement, je suis nouvellement inscrit ici, je ne peux donc pas commenter sa réponse, alors écrivez-la ici. Je ne suis pas sûr de bien comprendre comment mettre en œuvre l'approche proposée pour résoudre mon problème avec Map
avec Class
(proposé par @Sotirios à la fin de sa réponse). Quelqu'un voudrait-il donner un exemple?
Non, ce n'est pas un bug. C'est le résultat du fonctionnement du hack ParameterizedTypeReference
.
Si vous regardez son implémentation, il utilise Class#getGenericSuperclass()
qui indique
Retourne le Type représentant la superclasse directe de l'entité (classe, interface, type primitif ou void) représentée par cette classe.
Si la superclasse est un type paramétré, l'objet
Type
renvoyé doit refléter avec précision les paramètres de type réels utilisés dans le code source.
Donc, si vous utilisez
new ParameterizedTypeReference<ResponseWrapper<MyClass>>() {}
il retournera avec précision un Type
pour ResponseWrapper<MyClass>
.
Si tu utilises
new ParameterizedTypeReference<ResponseWrapper<T>>() {}
il retournera avec précision un Type
pour ResponseWrapper<T>
parce que c'est ainsi qu'il apparaît dans le code source.
Lorsque Spring voit T
, qui est en fait un objet TypeVariable
, il ne connaît pas le type à utiliser et utilise donc sa valeur par défaut.
Vous ne pouvez pas utiliser ParameterizedTypeReference
comme vous le proposez, ce qui le rend générique dans le sens où il accepte n'importe quel type. Envisagez d'écrire un Map
avec la clé Class
mappée sur un ParameterizedTypeReference
prédéfini pour cette classe.
Vous pouvez sous-classer ParameterizedTypeReference
et redéfinir sa méthode getType
pour renvoyer un ParameterizedType
correctement créé, comme suggéré par IonSpin .
Comme le montre le code ci-dessous, cela fonctionne.
public <T> ResponseWrapper<T> makeRequest(URI uri, final Class<T> clazz) {
ResponseEntity<ResponseWrapper<T>> response = template.exchange(
uri,
HttpMethod.POST,
null,
new ParameterizedTypeReference<ResponseWrapper<T>>() {
public Type getType() {
return new MyParameterizedTypeImpl((ParameterizedType) super.getType(), new Type[] {clazz});
}
});
return response;
}
public class MyParameterizedTypeImpl implements ParameterizedType {
private ParameterizedType delegate;
private Type[] actualTypeArguments;
MyParameterizedTypeImpl(ParameterizedType delegate, Type[] actualTypeArguments) {
this.delegate = delegate;
this.actualTypeArguments = actualTypeArguments;
}
@Override
public Type[] getActualTypeArguments() {
return actualTypeArguments;
}
@Override
public Type getRawType() {
return delegate.getRawType();
}
@Override
public Type getOwnerType() {
return delegate.getOwnerType();
}
}
Comme l'explique Sotirios, vous ne pouvez pas utiliser ParameterizedTypeReference
, mais ParameterizedTypeReference est utilisé uniquement pour fournir Type
au mappeur d'objet. créez votre propre ParameterizedType
et transmettez-le à RestTemplate
afin que le mappeur d'objet puisse reconstruire l'objet dont vous avez besoin.
Tout d'abord, vous devez avoir implémenté l'interface ParameterizedType. Vous pouvez trouver une implémentation dans le projet Google Gson ici . Une fois que vous avez ajouté l'implémentation à votre projet, vous pouvez étendre le résumé ParameterizedTypeReference
comme ceci:
class FakeParameterizedTypeReference<T> extends ParameterizedTypeReference<T> {
@Override
public Type getType() {
Type [] responseWrapperActualTypes = {MyClass.class};
ParameterizedType responseWrapperType = new ParameterizedTypeImpl(
ResponseWrapper.class,
responseWrapperActualTypes,
null
);
return responseWrapperType;
}
}
Et puis vous pouvez passer cela à votre fonction d’échange:
template.exchange(
uri,
HttpMethod.POST,
null,
new FakeParameterizedTypeReference<ResponseWrapper<T>>());
Avec toutes les informations de type, le mappeur d'objet présent construira correctement votre ResponseWrapper<MyClass>
objet
En fait, vous pouvez le faire, mais avec du code supplémentaire.
Il y a Guava équivalent de ParameterizedTypeReference et il s'appelle TypeToken .
La classe de goyave est beaucoup plus puissante que celle de Spring. Vous pouvez composer les TypeTokens à votre guise. Par exemple:
static <K, V> TypeToken<Map<K, V>> mapToken(TypeToken<K> keyToken, TypeToken<V> valueToken) {
return new TypeToken<Map<K, V>>() {}
.where(new TypeParameter<K>() {}, keyToken)
.where(new TypeParameter<V>() {}, valueToken);
}
Si vous appelez mapToken(TypeToken.of(String.class), TypeToken.of(BigInteger.class));
, vous créerez TypeToken<Map<String, BigInteger>>
!
Le seul inconvénient ici est que beaucoup d'API Spring nécessitent ParameterizedTypeReference
et non pas TypeToken
. Mais nous pouvons créer ParameterizedTypeReference
implémentation qui est un adaptateur pour TypeToken
lui-même.
import com.google.common.reflect.TypeToken;
import org.springframework.core.ParameterizedTypeReference;
import Java.lang.reflect.Type;
public class ParameterizedTypeReferenceBuilder {
public static <T> ParameterizedTypeReference<T> fromTypeToken(TypeToken<T> typeToken) {
return new TypeTokenParameterizedTypeReference<>(typeToken);
}
private static class TypeTokenParameterizedTypeReference<T> extends ParameterizedTypeReference<T> {
private final Type type;
private TypeTokenParameterizedTypeReference(TypeToken<T> typeToken) {
this.type = typeToken.getType();
}
@Override
public Type getType() {
return type;
}
@Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof ParameterizedTypeReference &&
this.type.equals(((ParameterizedTypeReference<?>) obj).getType())));
}
@Override
public int hashCode() {
return this.type.hashCode();
}
@Override
public String toString() {
return "ParameterizedTypeReference<" + this.type + ">";
}
}
}
Ensuite, vous pouvez l'utiliser comme ceci:
public <T> ResponseWrapper<T> makeRequest(URI uri, Class<T> clazz) {
ParameterizedTypeReference<ResponseWrapper<T>> responseTypeRef =
ParameterizedTypeReferenceBuilder.fromTypeToken(
new TypeToken<ResponseWrapper<T>>() {}
.where(new TypeParameter<T>() {}, clazz));
ResponseEntity<ResponseWrapper<T>> response = template.exchange(
uri,
HttpMethod.POST,
null,
responseTypeRef);
return response;
}
Et appelez ça comme:
ResponseWrapper<MyClass> result = makeRequest(uri, MyClass.class);
Et le corps de la réponse sera correctement désérialisé sous la forme ResponseWrapper<MyClass>
!
Vous pouvez même utiliser des types plus complexes si vous réécrivez votre méthode de requête générique (ou la surchargez) comme ceci:
public <T> ResponseWrapper<T> makeRequest(URI uri, TypeToken<T> resultTypeToken) {
ParameterizedTypeReference<ResponseWrapper<T>> responseTypeRef =
ParameterizedTypeReferenceBuilder.fromTypeToken(
new TypeToken<ResponseWrapper<T>>() {}
.where(new TypeParameter<T>() {}, resultTypeToken));
ResponseEntity<ResponseWrapper<T>> response = template.exchange(
uri,
HttpMethod.POST,
null,
responseTypeRef);
return response;
}
De cette façon, T
peut être de type complexe, comme List<MyClass>
.
Et appelez ça comme:
ResponseWrapper<List<MyClass>> result = makeRequest(uri, new TypeToken<List<MyClass>>() {});
J'utilise org.springframework.core.ResolvableType pour une ListResultEntity:
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(ListResultEntity.class, itemClass);
ParameterizedTypeReference<ListResultEntity<T>> typeRef = ParameterizedTypeReference.forType(resolvableType.getType());
Donc dans votre cas:
public <T> ResponseWrapper<T> makeRequest(URI uri, Class<T> clazz) {
ResponseEntity<ResponseWrapper<T>> response = template.exchange(
uri,
HttpMethod.POST,
null,
ParameterizedTypeReference.forType(ResolvableType.forClassWithGenerics(ResponseWrapper.class, clazz)));
return response;
}
Cela utilise uniquement spring et nécessite bien sûr une connaissance des types retournés (mais devrait même fonctionner pour des choses comme Wrapper >> tant que vous fournissez les classes sous la forme varargs)
J'ai un autre moyen de le faire ... supposons que vous convertissiez votre convertisseur de messages en chaîne pour votre RestTemplate, vous pourrez alors recevoir du JSON brut. En utilisant le JSON brut, vous pouvez ensuite le mapper dans votre collection générique à l'aide d'un mappeur d'objets Jackson. Voici comment:
Échangez le convertisseur de message:
List<HttpMessageConverter<?>> oldConverters = new ArrayList<HttpMessageConverter<?>>();
oldConverters.addAll(template.getMessageConverters());
List<HttpMessageConverter<?>> stringConverter = new ArrayList<HttpMessageConverter<?>>();
stringConverter.add(new StringHttpMessageConverter());
template.setMessageConverters(stringConverter);
Ensuite, obtenez votre réponse JSON comme ceci:
ResponseEntity<String> response = template.exchange(uri, HttpMethod.GET, null, String.class);
Traitez la réponse comme ceci:
String body = null;
List<T> result = new ArrayList<T>();
ObjectMapper mapper = new ObjectMapper();
if (response.hasBody()) {
body = items.getBody();
try {
result = mapper.readValue(body, mapper.getTypeFactory().constructCollectionType(List.class, clazz));
} catch (Exception e) {
e.printStackTrace();
} finally {
template.setMessageConverters(oldConverters);
}
...
Ma propre implémentation de l'appel générique restTemplate:
private <REQ, RES> RES queryRemoteService(String url, HttpMethod method, REQ req, Class reqClass) {
RES result = null;
try {
long startMillis = System.currentTimeMillis();
// Set the Content-Type header
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setContentType(new MediaType("application","json"));
// Set the request entity
HttpEntity<REQ> requestEntity = new HttpEntity<>(req, requestHeaders);
// Create a new RestTemplate instance
RestTemplate restTemplate = new RestTemplate();
// Add the Jackson and String message converters
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
restTemplate.getMessageConverters().add(new StringHttpMessageConverter());
// Make the HTTP POST request, marshaling the request to JSON, and the response to a String
ResponseEntity<RES> responseEntity = restTemplate.exchange(url, method, requestEntity, reqClass);
result = responseEntity.getBody();
long stopMillis = System.currentTimeMillis() - startMillis;
Log.d(TAG, method + ":" + url + " took " + stopMillis + " ms");
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
return result;
}
Pour ajouter un peu de contexte, je consomme le service RESTful avec ceci. Par conséquent, toutes les demandes et réponses sont regroupées dans un petit POJO comme ceci:
public class ValidateRequest {
User user;
User checkedUser;
Vehicle vehicle;
}
et
public class UserResponse {
User user;
RequestResult requestResult;
}
La méthode qui appelle cela est la suivante:
public User checkUser(User user, String checkedUserName) {
String url = urlBuilder()
.add(USER)
.add(USER_CHECK)
.build();
ValidateRequest request = new ValidateRequest();
request.setUser(user);
request.setCheckedUser(new User(checkedUserName));
UserResponse response = queryRemoteService(url, HttpMethod.POST, request, UserResponse.class);
return response.getUser();
}
Et oui, il existe également une liste dto-s.
Remarque: cette réponse se réfère/ajoute à la réponse et au commentaire de Sotirios Delimanolis.
J'ai essayé de le faire fonctionner avec Map<Class, ParameterizedTypeReference<ResponseWrapper<?>>>
, comme indiqué dans le commentaire de Sotirios, mais ne pourrait pas sans exemple.
Finalement, j'ai abandonné le caractère générique et le paramétrage de ParameterizedTypeReference et utilisé des types bruts à la place, comme ça
Map<Class<?>, ParameterizedTypeReference> typeReferences = new HashMap<>();
typeReferences.put(MyClass1.class, new ParameterizedTypeReference<ResponseWrapper<MyClass1>>() { });
typeReferences.put(MyClass2.class, new ParameterizedTypeReference<ResponseWrapper<MyClass2>>() { });
...
ParameterizedTypeReference typeRef = typeReferences.get(clazz);
ResponseEntity<ResponseWrapper<T>> response = restTemplate.exchange(
uri,
HttpMethod.GET,
null,
typeRef);
et cela a finalement fonctionné.
Si quelqu'un a un exemple avec la paramétrisation, je serais très reconnaissant de le voir.