在具有泛型参数的泛型方法中使用 Spring RestTemplate

Posted

技术标签:

【中文标题】在具有泛型参数的泛型方法中使用 Spring RestTemplate【英文标题】:Using Spring RestTemplate in generic method with generic parameter 【发布时间】:2014-03-26 02:06:52 【问题描述】:

要在 Spring RestTemplate 中使用泛型类型,我们需要使用 ParameterizedTypeReference (Unable to get a generic ResponseEntity<T> where T is a generic class "SomeClass<SomeGenericType>")

假设我有一些课

public class MyClass 
    int users[];

    public int[] getUsers()  return users; 
    public void setUsers(int[] users) this.users = users;

还有一些包装类

public class ResponseWrapper <T> 
    T response;

    public T getResponse ()  return response; 
    public void setResponse(T response) this.response = response;

所以,如果我尝试做这样的事情,一切都很好。

public ResponseWrapper<MyClass> makeRequest(URI uri) 
    ResponseEntity<ResponseWrapper<MyClass>> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        new ParameterizedTypeReference<ResponseWrapper<MyClass>>() );
    return response;

但是当我尝试创建上述方法的通用变体时...

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;

...并像这样调用此方法...

makeRequest(uri, MyClass.class)

... 而不是得到ResponseEntity&lt;ResponseWrapper&lt;MyClass&gt;&gt; 对象我得到ResponseEntity&lt;ResponseWrapper&lt;LinkedHashSet&gt;&gt; 对象。

我该如何解决这个问题?是 RestTemplate 错误吗?

更新 1 感谢@Sotirios,我理解了这个概念。不幸的是,我刚在这里注册,所以我无法评论他的答案,所以在这里写。我不确定我是否清楚地了解如何使用MapClass 键(由@Sotirios 在他的回答末尾提出)解决我的问题。有人介意举个例子吗?

【问题讨论】:

您应该始终能够评论您的问题的答案。建议的解决方案是这样工作的。您创建一个Map&lt;Class, ParameterizedTypeReference&lt;ResponseWrapper&lt;?&gt;&gt;&gt;。您将预构建的 ParameterizedTypeReference 实例添加到您期望的实际类型的 Map 对应的 Class @SotiriosDelimanolis - 你能为刚接触 Java 的人改写一下吗?我来自 C# 背景,所以这对我来说是非常有趣的经历。 【参考方案1】:
Abc is come object.

HttpEntity<Abc> httpEntity= new HttpEntity<>( headers );
ResponseEntity<Abc> resp = null;

resp = restCall( doUrl, HttpMethod.GET, httpEntity, new ParameterizedTypeReference<Abc>()  );
//----------------------------------------------

public <T> ResponseEntity restCall( String doUrl, HttpMethod httpMethod, HttpEntity<?> httpEntity, ParameterizedTypeReference respRef )
    
    try 
        return restTemplate.exchange( doUrl, httpMethod, httpEntity, respRef );
        
    catch( HttpClientErroException exc )
        
        do whatever
        
    

//--------------------------  can also use a generic inside
public class ComResp<T> 
    private T data;
    public ComResp( T data )
     this.data = data 


ResponseEntity<ComResp<Abc>> resp = null;

resp = restCall( doUrl, HttpMethod.GET, httpEntity, new ParameterizedTypeReference<ComResp<Abc>>()  );

// spring boot 2.5.3

【讨论】:

【参考方案2】:

我发现这是一个更优雅的解决方案:

private static <T> ParameterizedTypeReference<BaseResponse<T>> typeReferenceOf ( Class<T> tClass ) 
    return ParameterizedTypeReference.forType( sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl.make( BaseResponse.class, new Type[] tClass , null ) );

例如,给定以下 BaseResponseResponseData 类:

@Getter
@Setter
public static class BaseResponse<T> 
    
    private ResponseData<T> response;
    
    public BaseResponse ()  
    
    public boolean hasData () 
        return response != null;
    

    public T data () 
        return response.data;
    
    


@Getter
@Setter
public static final class ResponseData<T> 
    
    private T data;
    
    public ResponseData ()  
    

并给出一个示例get 方法,使用WebClient

public <T> Mono <T> get ( URI uri, Class<T> tClass ) 
    
    return webClient
        .get            ()
        .uri            ( uriBuilder        -> uriBuilder.pathSegment( uri.getPath() ).build() )
        .exchangeToMono ( clientResponse    -> clientResponse.bodyToMono( typeReferenceOf( tClass ) ) )
        .flatMap        ( baseResponse      -> baseResponse.hasData() ? Mono.just( baseResponse.data() ) : Mono.empty()  );
    

【讨论】:

【参考方案3】:

我觉得有一种更简单的方法可以做到这一点......只需使用您想要的类型参数定义一个类。例如:


final class MyClassWrappedByResponse extends ResponseWrapper<MyClass> 
    private static final long serialVersionUID = 1L;

现在将上面的代码更改为此,它应该可以工作:

public ResponseWrapper<MyClass> makeRequest(URI uri) 
    ResponseEntity<MyClassWrappedByResponse> response = template.exchange(
        uri,
        HttpMethod.POST,
        null,
        MyClassWrappedByResponse.class
    return response;

【讨论】:

【参考方案4】:

我自己的通用 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;

为了添加一些上下文,我正在使用 RESTful 服务,因此所有请求和响应都被包装到像这样的小型 POJO 中:

public class ValidateRequest 
  User user;
  User checkedUser;
  Vehicle vehicle;

public class UserResponse 
  User user;
  RequestResult requestResult;

调用它的方法如下:

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

是的,还有一个 List dto-s。

【讨论】:

【参考方案5】:

我正在使用 org.springframework.core.ResolvableType 作为 ListResultEntity :

    ResolvableType resolvableType = ResolvableType.forClassWithGenerics(ListResultEntity.class, itemClass);
    ParameterizedTypeReference<ListResultEntity<T>> typeRef = ParameterizedTypeReference.forType(resolvableType.getType());

所以在你的情况下:

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;

这仅使用 spring,当然需要一些关于返回类型的知识(但甚至应该适用于 Wrapper>> 之类的东西,只要你将类作为 varargs 提供)

【讨论】:

我想你的意思是 ResolvableType.forClassWithGenerics(ResponseWrapper.class, clazz).getType() ,对吧?您的帖子正在我的项目中工作,我认为它是最短和最简单的。谢谢贾斯汀! @Blangero 我收到 java.lang.IllegalArgumentException:为您的评论指定的泛型数量不匹配。【参考方案6】:

实际上,您可以这样做,但需要额外的代码。

Guava 等同于 ParameterizedTypeReference,它被称为 TypeToken。

Guava 的类比 Spring 的类更强大。 您可以根据需要编写 TypeToken。 例如:

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

如果您调用mapToken(TypeToken.of(String.class), TypeToken.of(BigInteger.class));,您将创建TypeToken&lt;Map&lt;String, BigInteger&gt;&gt;

这里唯一的缺点是许多 Spring API 需要 ParameterizedTypeReference 而不是 TypeToken。但是我们可以创建ParameterizedTypeReference 实现,它是TypeToken 本身的适配器。

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 + ">";
        

    


那么你可以这样使用它:

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;

然后这样称呼它:

ResponseWrapper<MyClass> result = makeRequest(uri, MyClass.class);

并且响应正文将被正确反序列化为ResponseWrapper&lt;MyClass&gt;

如果您像这样重写您的通用请求方法(或重载它),您甚至可以使用更复杂的类型:

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;

这样T 可以是复杂类型,比如List&lt;MyClass&gt;

然后这样称呼它:

ResponseWrapper<List<MyClass>> result = makeRequest(uri, new TypeToken<List<MyClass>>() );

【讨论】:

这适用于我的用例,这是一种使用 GitHub 样式的链接头解析分页数据的通用机制。它为我提供了一种创建有效类型以传递给 Spring HttpMessageConverterExtractor 的方法,让我可以将接口与我的类保持通用。 多么棒的解决方案!像魅力一样工作。 从 Spring 4.3.12 开始,您可以使用ParameterizedTypeReference.forType(typeToken.getType())【参考方案7】:

不,这不是错误。这是ParameterizedTypeReference hack 工作方式的结果。

如果你查看它的实现,它使用Class#getGenericSuperclass() 声明

返回表示实体的直接超类的类型 (类、接口、原始类型或 void)由该类表示。

如果超类是参数化类型,返回Type对象 必须准确反映源中使用的实际类型参数 代码。

所以,如果你使用

new ParameterizedTypeReference<ResponseWrapper<MyClass>>() 

它将准确地为ResponseWrapper&lt;MyClass&gt; 返回一个Type

如果你使用

new ParameterizedTypeReference<ResponseWrapper<T>>() 

它将准确地为ResponseWrapper&lt;T&gt; 返回一个Type,因为它在源代码中就是这样显示的。

当 Spring 看到 T,实际上是一个 TypeVariable 对象时,它不知道要使用的类型,所以它使用它的默认值。

你不能像你提议的那样使用ParameterizedTypeReference,让它在接受任何类型的意义上都是通用的。考虑编写一个 Map,其中键 Class 映射到该类的预定义 ParameterizedTypeReference

您可以继承ParameterizedTypeReference 并覆盖其getType 方法以返回适当创建的ParameterizedType、as suggested by IonSpin。

【讨论】:

但是他是硬编码的类(MyClass.class)我想通过动态的类也可以是 List 在我的情况下......你的建议很感激。 这个答案的缺点是什么(***.com/a/41629503/3656056)它适用于 CustomObject 但对于 CustomObject> 失败 @VelNaga 没有Class 对象为List&lt;MyClass&gt;,这样的事情不存在。 List&lt;MyClass&gt; 可以有一个 ParameterizedType,但您要么必须自己构建它,要么使用类型标记技巧。 @VelNaga 该解决方案失败的原因与您的尝试相同,List&lt;MyClass&gt; 不存在。由于他们的方法接受Class 参数,他们只能“嵌套”一个类型参数。 @VelNaga TypeToken in Gson,TypeReference in Jackson,ParameterizedTypeReference in RestTemplate,都是一样的模式,叫做type token【参考方案8】:

正如下面的代码所示,它可以工作。

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


【讨论】:

您的代码需要说明。请查看 SO 关于如何回答的政策。 事实上,这是迄今为止我见过的最好/最简单的答案。您只需要使用所示的 MyParameterizedTypeImpl 并且不需要具有实际类型和类型包装器的静态 Map。当然类必须传入方法或构造函数。 嗨,感谢您的回答,它适用于 CustomObject,但它对 CustomObject> 失败,您能帮我解决 CustomObject> 这个答案解释不清,解决方案有限。您使用Class&lt;T&gt; 参数这一事实将解决方案限制为非泛型类型。 RestTemplate java.lang.ClassCastException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl cannot be cast to java.lang.reflect.ParameterizedType 出现以下错误【参考方案9】:

注意:此答案引用/添加到 Sotirios Delimanolis 的答案和评论。

我试图让它与 Map&lt;Class, ParameterizedTypeReference&lt;ResponseWrapper&lt;?&gt;&gt;&gt; 一起工作,正如 Sotirios 的评论中所指出的那样,但不能没有一个例子。

最后,我从 ParameterizedTypeReference 中删除了通配符和参数化,改用原始类型,就像这样

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

这终于奏效了。

如果有人有参数化的示例,我将非常感激。

【讨论】:

【参考方案10】:

我有另一种方法可以做到这一点...假设您将消息转换器换成 String 用于 RestTemplate,那么您可以接收原始 JSON。使用原始 JSON,您可以使用 Jackson 对象映射器将其映射到您的通用集合中。方法如下:

换掉消息转换器:

    List<HttpMessageConverter<?>> oldConverters = new ArrayList<HttpMessageConverter<?>>();
    oldConverters.addAll(template.getMessageConverters());

    List<HttpMessageConverter<?>> stringConverter = new ArrayList<HttpMessageConverter<?>>();
    stringConverter.add(new StringHttpMessageConverter());

    template.setMessageConverters(stringConverter);

然后像这样得到你的 JSON 响应:

    ResponseEntity<String> response = template.exchange(uri, HttpMethod.GET, null, String.class);

像这样处理响应:

     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);
        
        ...

【讨论】:

我最喜欢这个解决方案。但什么是“clazz”?我以某种方式猜测它是T,但我无法表达它...... ObjectMapper 工厂的constructCollectionType 方法有两个输入,第一个是Collection 类,第二个(我标记为“clazz”)是上述Collection 类中包含的对象的类。 【参考方案11】:

正如 Sotirios 解释的那样,您不能使用 ParameterizedTypeReference,但 ParameterizedTypeReference 仅用于将 Type 提供给对象映射器,并且由于您拥有在类型擦除发生时被删除的类,您可以创建自己的ParameterizedType 并将其传递给RestTemplate,以便对象映射器可以重建您需要的对象。

首先你需要实现 ParameterizedType 接口,你可以在 Google Gson 项目here 中找到一个实现。 将实现添加到项目后,您可以像这样扩展抽象 ParameterizedTypeReference

class FakeParameterizedTypeReference<T> extends ParameterizedTypeReference<T> 

@Override
public Type getType() 
    Type [] responseWrapperActualTypes = MyClass.class;
    ParameterizedType responseWrapperType = new ParameterizedTypeImpl(
        ResponseWrapper.class,
        responseWrapperActualTypes,
        null
        );
    return responseWrapperType;
    

然后你可以将它传递给你的交换函数:

template.exchange(
    uri,
    HttpMethod.POST,
    null,
    new FakeParameterizedTypeReference<ResponseWrapper<T>>());

使用所有类型信息,对象映射器将正确构造您的 ResponseWrapper&lt;MyClass&gt; 对象

【讨论】:

JDK 中的 ParameterizedTypeImpl 类对其构造函数具有私有访问权限,并且没有设置器。您为此使用过图书馆吗?如果没有反射,我看不到任何实例化该类的方法...... 我从 Google Gson 项目中复制了 ParametrizedTypeImpl 实现,链接在答案中,同样在这里code.google.com/p/google-gson/source/browse/trunk/gson/src/main/… :) 啊,对不起,你完全正确。非常感谢! :) 此外,JDK 中的 ParameterizedTypeImpl 类具有私有构造函数,因为它旨在用作单例。它的 API 有一个 make 方法,您可以使用它来创建 ParameterizedType。

以上是关于在具有泛型参数的泛型方法中使用 Spring RestTemplate的主要内容,如果未能解决你的问题,请参考以下文章

GetMethod 的 Type[] 以获取具有两个参数和一个类型参数的泛型方法

TypeScript 中具有泛型类型参数的泛型类型的替代方案

Swift - 在具有可选参数的泛型函数中以 Nil 作为参数

如何根据T创建不同行为的泛型方法?

具有逆变类型参数和 Unity 容器的泛型

Java中的泛型理解