web-dev-qa-db-fra.com

Utilisation de Spring RestTemplate dans une méthode générique avec un paramètre générique

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?

50
Artyom Kozhemiakin

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 .

46
Sotirios Delimanolis

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();
    }

}
14
김원겸

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

9
IonSpin

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>>() {});
8

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)

5
Justin

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);
        }
        ...
2
Dilettante44

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.

1
AlexV

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.

0
Ginkobonsai