Spring Boot使用RestTemplate消费REST服务的几个问题记录

我们可以通过 Spring Boot 快速开发 REST 接口,同时也可能需要在实现接口的过程中,通过 Spring Boot 调用内外部 REST 接口完成业务逻辑。

在 Spring Boot 中,调用 REST Api 常见的一般主要有两种方式,通过自带的 RestTemplate 或者自己开发 http 客户端工具实现服务调用。

RestTemplate 基本功能非常强大,不过某些特殊场景,我们可能还是更习惯用自己封装的工具类,比如上传文件至分布式文件系统、处理带证书的 https 请求等。

本文以 RestTemplate 来举例,记录几个使用 RestTemplate 调用接口过程中发现的问题和解决方案。

一、RestTemplate 简介

1、什么是 RestTemplate

我们自己封装的 HttpClient,通常都会有一些模板代码,比如建立连接,构造请求头和请求体,然后根据响应,解析响应信息,最后关闭连接。

RestTemplate 是 Spring 中对 HttpClient 的再次封装,简化了发起 HTTP 请求以及处理响应的过程,抽象层级更高,减少消费者的模板代码,使冗余代码更少。

其实仔细想想 Spring Boot 下的很多 XXXTemplate 类,它们也提供各种模板方法,只不过抽象的层次更高,隐藏了更多细节而已。

顺便提一下,Spring Cloud 有一个声明式服务调用 Feign, 是基于 Netflix Feign 实现的,整合了 Spring Cloud Ribbon 与 Spring Cloud Hystrix,并且实现了声明式的 Web 服务客户端定义方式。

本质上 Feign 是在 RestTemplate 的基础上对其再次封装,由它来帮助我们定义和实现依赖服务接口的定义。

2、RestTemplate 常见方法

常见的 REST 服务有很多种请求方式,如 GET,POST,PUT,DELETE,HEAD,OPTIONS 等。RestTemplate 实现了最常见的方式,用的最多的就是 Get 和 Post 了,调用 API 可参考源码,这里列举几个方法定义(GET、POST、DELETE):

public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) 

public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)

public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables)

public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Object... uriVariables)

public void delete(String url, Object... uriVariables)

public void delete(URI url)

methods

同时要注意两个较为“灵活”的方法exchangeexecute

RestTemplate 暴露的 exchange 与其它接口的不同:

(1)允许调用者指定 HTTP 请求的方法(GET,POST,DELETE 等)

(2)可以在请求中增加 body 以及头信息,其内容通过参数‘HttpEntity<?>requestEntity’描述

(3)exchange 支持‘含参数的类型’(即泛型类)作为返回类型,该特性通过‘ParameterizedTypeReference<T>responseType’描述。

RestTemplate 所有的 GET,POST 等等方法,最终调用的都是 execute 方法。excute 方法的内部实现是将 String 格式的 URI 转成了 java.net.URI,之后调用了 doExecute 方法,doExecute 方法的实现如下:

    /**
     * Execute the given method on the provided URI.
     * <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
     * the response with the {@link ResponseExtractor}.
     * @param url the fully-expanded URL to connect to
     * @param method the HTTP method to execute (GET, POST, etc.)
     * @param requestCallback object that prepares the request (can be {@code null})
     * @param responseExtractor object that extracts the return value from the response (can be {@code null})
     * @return an arbitrary object, as returned by the {@link ResponseExtractor}
     */
    @Nullable
    protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
            @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
    Assert.notNull(url, </span>"'url' must not be null"<span style="color: rgba(0, 0, 0, 1)">);
    Assert.notNull(method, </span>"'method' must not be null"<span style="color: rgba(0, 0, 0, 1)">);
    ClientHttpResponse response </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
        ClientHttpRequest request </span>=<span style="color: rgba(0, 0, 0, 1)"> createRequest(url, method);
        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (requestCallback != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            requestCallback.doWithRequest(request);
        }
        response </span>=<span style="color: rgba(0, 0, 0, 1)"> request.execute();
        handleResponse(url, method, response);
        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (responseExtractor != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> responseExtractor.extractData(response);
        }
        </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
        }
    }
    </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (IOException ex) {
        String resource </span>=<span style="color: rgba(0, 0, 0, 1)"> url.toString();
        String query </span>=<span style="color: rgba(0, 0, 0, 1)"> url.getRawQuery();
        resource </span>= (query != <span style="color: rgba(0, 0, 255, 1)">null</span> ? resource.substring(0, resource.indexOf('?'<span style="color: rgba(0, 0, 0, 1)">)) : resource);
        </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ResourceAccessException("I/O error on " + method.name() +
                " request for \"" + resource + "\": " +<span style="color: rgba(0, 0, 0, 1)"> ex.getMessage(), ex);
    }
    </span><span style="color: rgba(0, 0, 255, 1)">finally</span><span style="color: rgba(0, 0, 0, 1)"> {
        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (response != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
            response.close();
        }
    }
}</span></pre>
doExecute

doExecute 方法封装了模板方法,比如创建连接、处理请求和应答,关闭连接等。

多数人看到这里,估计都会觉得封装一个 RestClient 不过如此吧?

3、简单调用

以一个 POST 调用为例:

package com.power.demo.restclient;

import com.power.demo.common.AppConst;
import com.power.demo.restclient.clientrequest.ClientGetGoodsByGoodsIdRequest;
import com.power.demo.restclient.clientresponse.ClientGetGoodsByGoodsIdResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

/**

  • 商品 REST 接口客户端 (demo 测试用)
    /
    @Component
    public class GoodsServiceClient {

    //服务消费者调用的接口 URL 形如:http://localhost:9090
    @Value("${spring.power.serviceurl}")
    private String _serviceUrl;

    @Autowired
    private RestTemplate restTemplate;

    public ClientGetGoodsByGoodsIdResponse getGoodsByGoodsId(ClientGetGoodsByGoodsIdRequest request) {
    String svcUrl
    = getGoodsSvcUrl() + "/getinfobyid";

     ClientGetGoodsByGoodsIdResponse response </span>= <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">;
    
     </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
         response </span>= restTemplate.postForObject(svcUrl, request, ClientGetGoodsByGoodsIdResponse.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">);
     } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) {
         e.printStackTrace();
         response </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ClientGetGoodsByGoodsIdResponse();
         response.setCode(AppConst.FAIL);
         response.setMessage(e.toString());
     }
    
     </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> response;
    

    }

    private String getGoodsSvcUrl() {

     String url </span>= ""<span style="color: rgba(0, 0, 0, 1)">;
    
     </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_serviceUrl == <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
         _serviceUrl </span>= ""<span style="color: rgba(0, 0, 0, 1)">;
     }
     </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_serviceUrl.length() == 0<span style="color: rgba(0, 0, 0, 1)">) {
         </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> url;
     }
    
     </span><span style="color: rgba(0, 0, 255, 1)">if</span> (_serviceUrl.substring(_serviceUrl.length() - 1, _serviceUrl.length()) == "/"<span style="color: rgba(0, 0, 0, 1)">) {
         url </span>= String.format("%sapi/v1/goods"<span style="color: rgba(0, 0, 0, 1)">, _serviceUrl);
     } </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
         url </span>= String.format("%s/api/v1/goods"<span style="color: rgba(0, 0, 0, 1)">, _serviceUrl);
     }
    
     </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> url;
    

    }

}

GoodsServiceClient

demo 里直接 RestTemplate.postForObject 方法调用,反序列化实体转换这些 RestTemplate 内部封装搞定。

二、问题汇总

1、no suitable HttpMessageConverter found for request type 异常

这个问题通常会出现在 postForObject 中传入对象进行调用的时候。

分析 RestTemplate 源码,在 HttpEntityRequestCallback 类的 doWithRequest 方法中,如果messageConverters(这个字段后面会继续提及)列表字段循环处理的过程中没有满足 return 跳出的逻辑(也就是没有匹配的 HttpMessageConverter),则抛出上述异常:

        @Override
        @SuppressWarnings("unchecked")
        public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
            super.doWithRequest(httpRequest);
            Object requestBody = this.requestEntity.getBody();
            if (requestBody == null) {
                HttpHeaders httpHeaders = httpRequest.getHeaders();
                HttpHeaders requestHeaders = this.requestEntity.getHeaders();
                if (!requestHeaders.isEmpty()) {
                    for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
                    }
                }
                if (httpHeaders.getContentLength() < 0) {
                    httpHeaders.setContentLength(0L);}
            }
            else {
                Class<?> requestBodyClass = requestBody.getClass();
                Type requestBodyType = (this.requestEntity instanceof RequestEntity ?
                        ((RequestEntity<?>)this.requestEntity).getType() : requestBodyClass);
                HttpHeaders httpHeaders = httpRequest.getHeaders();
                HttpHeaders requestHeaders = this.requestEntity.getHeaders();
                MediaType requestContentType = requestHeaders.getContentType();
                for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
                    if (messageConverter instanceof GenericHttpMessageConverter) {
                        GenericHttpMessageConverter<Object> genericConverter =
                                (GenericHttpMessageConverter<Object>) messageConverter;
                        if (genericConverter.canWrite(requestBodyType, requestBodyClass, requestContentType)) {
                            if (!requestHeaders.isEmpty()) {
                                for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {httpHeaders.put(entry.getKey(), new LinkedList<>(entry.getValue()));
                                }
                            }
                            if (logger.isDebugEnabled()) {
                                if (requestContentType != null) {
                                    logger.debug("Writing [" + requestBody + "] as \""+ requestContentType +"\"using [" + messageConverter + "]");}
                                else {
                                    logger.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");}
                        }
                        genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
                        </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
                    }
                }
                </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (messageConverter.canWrite(requestBodyClass, requestContentType)) {
                    </span><span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 0, 1)">requestHeaders.isEmpty()) {
                        </span><span style="color: rgba(0, 0, 255, 1)">for</span> (Map.Entry&lt;String, List&lt;String&gt;&gt;<span style="color: rgba(0, 0, 0, 1)"> entry : requestHeaders.entrySet()) {
                            httpHeaders.put(entry.getKey(), </span><span style="color: rgba(0, 0, 255, 1)">new</span> LinkedList&lt;&gt;<span style="color: rgba(0, 0, 0, 1)">(entry.getValue()));
                        }
                    }
                    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (logger.isDebugEnabled()) {
                        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (requestContentType != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
                            logger.debug(</span>"Writing [" + requestBody + "] as \"" + requestContentType +
                                    "\" using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">);
                        }
                        </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
                            logger.debug(</span>"Writing [" + requestBody + "] using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">);
                        }

                    }
                    ((HttpMessageConverter</span>&lt;Object&gt;<span style="color: rgba(0, 0, 0, 1)">) messageConverter).write(
                            requestBody, requestContentType, httpRequest);
                    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
                }
            }
            String message </span>= "Could not write request: no suitable HttpMessageConverter found for request type [" +<span style="color: rgba(0, 0, 0, 1)">
                    requestBodyClass.getName() </span>+ "]"<span style="color: rgba(0, 0, 0, 1)">;
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (requestContentType != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
                message </span>+= " and content type [" + requestContentType + "]"<span style="color: rgba(0, 0, 0, 1)">;
            }
            </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> RestClientException(message);
        }
    }</span></pre>
HttpEntityRequestCallback.doWithRequest

最简单的解决方案是,可以通过包装 http 请求头,并将请求对象序列化成字符串的形式传参,参考示例代码如下:

    /*
     * Post 请求调用
     * */
    public static String postForObject(RestTemplate restTemplate, String url, Object params) {
        HttpHeaders headers = new HttpHeaders();
        MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
        headers.setContentType(type);
        headers.add("Accept", MediaType.APPLICATION_JSON.toString());
    String json </span>=<span style="color: rgba(0, 0, 0, 1)"> SerializeUtil.Serialize(params);

    HttpEntity</span>&lt;String&gt; formEntity = <span style="color: rgba(0, 0, 255, 1)">new</span> HttpEntity&lt;String&gt;<span style="color: rgba(0, 0, 0, 1)">(json, headers);

    String result </span>= restTemplate.postForObject(url, formEntity, String.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> result;
}</span></pre>
postForObject

如果我们还想直接返回对象,直接反序列化返回的字符串即可:

    /*
     * Post 请求调用
     * */
    public static <T> T postForObject(RestTemplate restTemplate, String url, Object params, Class<T> clazz) {
        T response = null;
    String respStr </span>=<span style="color: rgba(0, 0, 0, 1)"> postForObject(restTemplate, url, params);

    response </span>=<span style="color: rgba(0, 0, 0, 1)"> SerializeUtil.DeSerialize(respStr, clazz);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> response;
}</span></pre>
postForObject

其中,序列化和反序列化工具比较多,常用的比如 fastjson、jackson 和 gson。

2、no suitable HttpMessageConverter found for response type 异常

和发起请求发生异常一样,处理应答的时候也会有问题。

StackOverflow 上有人问过相同的问题,根本原因是 HTTP 消息转换器 HttpMessageConverter 缺少MIME Type,也就是说 HTTP 在把输出结果传送到客户端的时候,客户端必须启动适当的应用程序来处理这个输出文档,这可以通过多种 MIME(多功能网际邮件扩充协议)Type 来完成。

对于服务端应答,很多 HttpMessageConverter 默认支持的媒体类型(MIMEType)都不同。StringHttpMessageConverter 默认支持的则是 MediaType.TEXT_PLAIN,SourceHttpMessageConverter 默认支持的则是 MediaType.TEXT_XML,FormHttpMessageConverter 默认支持的是 MediaType.APPLICATION_FORM_URLENCODED 和 MediaType.MULTIPART_FORM_DATA,在 REST 服务中,我们用到的最多的还是MappingJackson2HttpMessageConverter,这是一个比较通用的转化器(继承自 GenericHttpMessageConverter 接口),根据分析,它默认支持的 MIMEType 为 MediaType.APPLICATION_JSON:

    /**
     * Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
     * You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
     * @see Jackson2ObjectMapperBuilder#json()
     */
    public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));}
MappingJackson2HttpMessageConverter

但是有些应用接口默认的应答 MIMEType 不是 application/json,比如我们调用一个外部天气预报接口,如果使用 RestTemplate 的默认配置,直接返回一个字符串应答是没有问题的:

String url = "http://wthrcdn.etouch.cn/weather_mini?city= 上海";
String result = restTemplate.getForObject(url, String.class);
ClientWeatherResultVO vo = SerializeUtil.DeSerialize(result, ClientWeatherResultVO.class);

但是,如果我们想直接返回一个实体对象:

String url = "http://wthrcdn.etouch.cn/weather_mini?city= 上海";

ClientWeatherResultVO weatherResultVO = restTemplate.getForObject(url, ClientWeatherResultVO.class);

则直接报异常:
Could not extract response: no suitable HttpMessageConverter found for response type [class]
and content type [application/octet-stream]

很多人碰到过这个问题,首次碰到估计大多都比较懵吧,很多接口都是 json 或者 xml 或者 plain text 格式返回的,什么是 application/octet-stream?

查看 RestTemplate 源代码,一路跟踪下去会发现HttpMessageConverterExtractor类的 extractData 方法有个解析应答及反序列化逻辑,如果不成功,抛出的异常信息和上述一致:

    @Override
    @SuppressWarnings({"unchecked", "rawtypes", "resource"})
    public T extractData(ClientHttpResponse response) throws IOException {
        MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
        if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
            return null;
        }
        MediaType contentType = getContentType(responseWrapper);
    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
        </span><span style="color: rgba(0, 0, 255, 1)">for</span> (HttpMessageConverter&lt;?&gt; messageConverter : <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.messageConverters) {
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (messageConverter <span style="color: rgba(0, 0, 255, 1)">instanceof</span><span style="color: rgba(0, 0, 0, 1)"> GenericHttpMessageConverter) {
                GenericHttpMessageConverter</span>&lt;?&gt; genericMessageConverter =<span style="color: rgba(0, 0, 0, 1)">
                        (GenericHttpMessageConverter</span>&lt;?&gt;<span style="color: rgba(0, 0, 0, 1)">) messageConverter;
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (genericMessageConverter.canRead(<span style="color: rgba(0, 0, 255, 1)">this</span>.responseType, <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, contentType)) {
                    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (logger.isDebugEnabled()) {
                        logger.debug(</span>"Reading [" + <span style="color: rgba(0, 0, 255, 1)">this</span>.responseType + "] as \"" +<span style="color: rgba(0, 0, 0, 1)">
                                contentType </span>+ "\" using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">);
                    }
                    </span><span style="color: rgba(0, 0, 255, 1)">return</span> (T) genericMessageConverter.read(<span style="color: rgba(0, 0, 255, 1)">this</span>.responseType, <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">, responseWrapper);
                }
            }
            </span><span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">this</span>.responseClass != <span style="color: rgba(0, 0, 255, 1)">null</span><span style="color: rgba(0, 0, 0, 1)">) {
                </span><span style="color: rgba(0, 0, 255, 1)">if</span> (messageConverter.canRead(<span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.responseClass, contentType)) {
                    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (logger.isDebugEnabled()) {
                        logger.debug(</span>"Reading [" + <span style="color: rgba(0, 0, 255, 1)">this</span>.responseClass.getName() + "] as \"" +<span style="color: rgba(0, 0, 0, 1)">
                                contentType </span>+ "\" using [" + messageConverter + "]"<span style="color: rgba(0, 0, 0, 1)">);
                    }
                    </span><span style="color: rgba(0, 0, 255, 1)">return</span> (T) messageConverter.read((Class) <span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">.responseClass, responseWrapper);
                }
            }
        }
    }
    </span><span style="color: rgba(0, 0, 255, 1)">catch</span> (IOException |<span style="color: rgba(0, 0, 0, 1)"> HttpMessageNotReadableException ex) {
        </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> RestClientException("Error while extracting response for type [" +
                <span style="color: rgba(0, 0, 255, 1)">this</span>.responseType + "] and content type [" + contentType + "]"<span style="color: rgba(0, 0, 0, 1)">, ex);
    }

    </span><span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> RestClientException("Could not extract response: no suitable HttpMessageConverter found " +
            "for response type [" + <span style="color: rgba(0, 0, 255, 1)">this</span>.responseType + "] and content type [" + contentType + "]"<span style="color: rgba(0, 0, 0, 1)">);
}</span></pre>
HttpMessageConverterExtractor.extractData

StackOverflow 上的解决的示例代码可以接受,但是并不准确,常见的 MIMEType 都应该加进去,贴一下我认为正确的代码:

package com.power.demo.restclient.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.*;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;

import java.util.Arrays;
import java.util.List;

@Component
public class RestTemplateConfig {

</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed"<span style="color: rgba(0, 0, 0, 1)">, RestTemplate
        .</span><span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span>.getClassLoader()) &amp;&amp; ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());
</span><span style="color: rgba(0, 0, 255, 1)">private</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> <span style="color: rgba(0, 0, 255, 1)">boolean</span> jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", RestTemplate.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">.getClassLoader());

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例</span>

@Autowired
private RestTemplateBuilder builder;

@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ObjectMapper objectMapper;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例</span>

@Bean
public RestTemplate restTemplate() {

    RestTemplate restTemplate </span>=<span style="color: rgba(0, 0, 0, 1)"> builder.build();

    List</span>&lt;HttpMessageConverter&lt;?&gt;&gt; messageConverters =<span style="color: rgba(0, 0, 0, 1)"> Lists.newArrayList();
    MappingJackson2HttpMessageConverter converter </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2HttpMessageConverter();
    converter.setObjectMapper(objectMapper);

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">不加会出现异常
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Could not extract response: no suitable HttpMessageConverter found for response type [class ]</span>
MediaType[] mediaTypes = new MediaType[]{ MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM,
            MediaType.APPLICATION_JSON_UTF8,
            MediaType.TEXT_HTML,
            MediaType.TEXT_PLAIN,
            MediaType.TEXT_XML,
            MediaType.APPLICATION_STREAM_JSON,
            MediaType.APPLICATION_ATOM_XML,
            MediaType.APPLICATION_FORM_URLENCODED,
            MediaType.APPLICATION_PDF,
    };

    converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));

    <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">messageConverters.add(converter);</span>
    <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2Present) {
        messageConverters.add(converter);
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (gsonPresent) {
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> GsonHttpMessageConverter());
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jsonbPresent) {
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> JsonbHttpMessageConverter());
    }

    messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> FormHttpMessageConverter());

    messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ByteArrayHttpMessageConverter());
    messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> StringHttpMessageConverter());
    messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span> ResourceHttpMessageConverter(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">));
    messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SourceHttpMessageConverter());
    messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> AllEncompassingFormHttpMessageConverter());
    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (romePresent) {
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> AtomFeedHttpMessageConverter());
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> RssChannelHttpMessageConverter());
    }

    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2XmlPresent) {
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2XmlHttpMessageConverter());
    } </span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jaxb2Present) {
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Jaxb2RootElementHttpMessageConverter());
    }


    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2SmilePresent) {
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2SmileHttpMessageConverter());
    }

    </span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (jackson2CborPresent) {
        messageConverters.add(</span><span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2CborHttpMessageConverter());
    }

    restTemplate.setMessageConverters(messageConverters);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate;
}

}

RestTemplateConfig

看到上面的代码,再对比一下 RestTemplate 内部实现,就知道我参考了 RestTemplate 的源码,有洁癖的人可能会说这一坨代码有点啰嗦,上面那一堆 static final 的变量和 messageConverters 填充数据方法,暴露了 RestTemplate 的实现,如果 RestTemplate 修改了,这里也要改,非常不友好,而且看上去一点也不 OO。

经过分析,RestTemplateBuilder.build() 构造了 RestTemplate 对象,只要将内部 MappingJackson2HttpMessageConverter 修改一下支持的 MediaType 即可,RestTemplate 的 messageConverters 字段虽然是 private final 的,我们依然可以通过反射修改之,改进后的代码如下:

package com.power.demo.restclient.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Component
public class RestTemplateConfig {

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例</span>

@Autowired
private RestTemplateBuilder builder;

@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ObjectMapper objectMapper;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例</span>

@Bean
public RestTemplate restTemplate() {

    RestTemplate restTemplate </span>=<span style="color: rgba(0, 0, 0, 1)"> builder.build();

    List</span>&lt;HttpMessageConverter&lt;?&gt;&gt; messageConverters =<span style="color: rgba(0, 0, 0, 1)"> Lists.newArrayList();
    MappingJackson2HttpMessageConverter converter </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2HttpMessageConverter();
    converter.setObjectMapper(objectMapper);

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">不加可能会出现异常
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Could not extract response: no suitable HttpMessageConverter found for response type [class ]</span>
MediaType[] mediaTypes = new MediaType[]{ MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM,
            MediaType.TEXT_HTML,
            MediaType.TEXT_PLAIN,
            MediaType.TEXT_XML,
            MediaType.APPLICATION_STREAM_JSON,
            MediaType.APPLICATION_ATOM_XML,
            MediaType.APPLICATION_FORM_URLENCODED,
            MediaType.APPLICATION_JSON_UTF8,
            MediaType.APPLICATION_PDF,
    };

    converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));

    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">通过反射设置MessageConverters</span>
        Field field = restTemplate.getClass().getDeclaredField("messageConverters"<span style="color: rgba(0, 0, 0, 1)">);

        field.setAccessible(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);

        List</span>&lt;HttpMessageConverter&lt;?&gt;&gt; orgConverterList = (List&lt;HttpMessageConverter&lt;?&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">) field.get(restTemplate);

        Optional</span>&lt;HttpMessageConverter&lt;?&gt;&gt; opConverter =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream()
                .filter(x </span>-&gt; x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">
                        .getName()))
                .findFirst();

        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (opConverter.isPresent() == <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate;
        }

        messageConverters.add(converter);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加MappingJackson2HttpMessageConverter

        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加原有的剩余的HttpMessageConverter</span>
        List&lt;HttpMessageConverter&lt;?&gt;&gt; leftConverters =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream()
                .filter(x </span>-&gt; x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">
                        .getName()) </span>== <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">)
                .collect(Collectors.toList());

        messageConverters.addAll(leftConverters);

        System.out.println(String.format(</span>"【HttpMessageConverter】原有数量:%s,重新构造后数量:%s"<span style="color: rgba(0, 0, 0, 1)">
                , orgConverterList.size(), messageConverters.size()));

    } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) {
        e.printStackTrace();
    }

    restTemplate.setMessageConverters(messageConverters);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate;
}

}

RestTemplateConfig

除了一个 messageConverters 字段,看上去我们不再关心 RestTemplate 那些外部依赖包和内部构造过程,果然干净简洁好维护了很多。

3、乱码问题

这个也是一个非常经典的问题。解决方案非常简单,找到 HttpMessageConverter,看看默认支持的 Charset。AbstractJackson2HttpMessageConverter 是很多 HttpMessageConverter 的基类,默认编码为 UTF-8:

public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> Charset DEFAULT_CHARSET =<span style="color: rgba(0, 0, 0, 1)"> StandardCharsets.UTF_8;

}

AbstractJackson2HttpMessageConverter

而 StringHttpMessageConverter 比较特殊,有人反馈过发生乱码问题由它默认支持的编码ISO-8859-1引起:

/**
 * Implementation of {@link HttpMessageConverter} that can read and write strings.
 *
 * <p>By default, this converter supports all media types ({@code &#42;&#47;&#42;}),
 * and writes with a {@code Content-Type} of {@code text/plain}. This can be overridden
 * by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property.
 *
 * @author Arjen Poutsma
 * @author Juergen Hoeller
 * @since 3.0
 */
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">final</span> Charset DEFAULT_CHARSET =<span style="color: rgba(0, 0, 0, 1)"> StandardCharsets.ISO_8859_1;

</span><span style="color: rgba(0, 128, 0, 1)">/**</span><span style="color: rgba(0, 128, 0, 1)">
 * A default constructor that uses {</span><span style="color: rgba(128, 128, 128, 1)">@code</span><span style="color: rgba(0, 128, 0, 1)"> "ISO-8859-1"} as the default charset.
 * </span><span style="color: rgba(128, 128, 128, 1)">@see</span><span style="color: rgba(0, 128, 0, 1)"> #StringHttpMessageConverter(Charset)
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span>
<span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> StringHttpMessageConverter() {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span><span style="color: rgba(0, 0, 0, 1)">(DEFAULT_CHARSET);
}

}

StringHttpMessageConverter

如果在使用过程中发生乱码,我们可以通过方法设置 HttpMessageConverter 支持的编码,常用的有 UTF-8、GBK 等。

4、反序列化异常

这是开发过程中容易碰到的又一个问题。因为 Java 的开源框架和工具类非常之多,而且版本更迭频繁,所以经常发生一些意想不到的坑。

以 joda time 为例,joda time 是流行的 java 时间和日期框架,但是如果你的接口对外暴露 joda time 的类型,比如 DateTime,那么接口调用方(同构和异构系统)可能会碰到序列化难题,反序列化时甚至直接抛出如下异常:

org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class org.joda.time.Chronology]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.joda.time.Chronology` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (PushbackInputStream);

我在前厂就碰到过,可以参考这里,后来为了调用方便,改回直接暴露 Java 的 Date 类型。

当然解决的方案不止这一种,可以使用 jackson 支持自定义类的序列化和反序列化的方式。在精度要求不是很高的系统里,实现简单的 DateTime 自定义序列化:

package com.power.demo.util;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.io.IOException;

/**

  • 在默认情况下,jackson 会将 joda time 序列化为较为复杂的形式,不利于阅读,并且对象较大。

  • <p>

  • JodaTime 序列化的时候可以将 datetime 序列化为字符串,更容易读
    /
    public class DateTimeSerializer extends JsonSerializer<DateTime> {

    private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH🇲🇲ss");

    @Override
    public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
    jgen.writeString(value.toString(dateFormatter));
    }
    }

DateTimeSerializer

以及 DateTime 反序列化:

package com.power.demo.util;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.io.IOException;

/**

  • JodaTime 反序列化将字符串转化为 datetime
    /
    public class DatetimeDeserializer extends JsonDeserializer<DateTime> {

    private static DateTimeFormatter dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH🇲🇲ss");

    @Override
    public DateTime deserialize(JsonParser jp, DeserializationContext context) throws IOException, JsonProcessingException {
    JsonNode node
    = jp.getCodec().readTree(jp);
    String s
    = node.asText();
    DateTime parse
    = DateTime.parse(s, dateFormatter);
    return parse;
    }
    }

DatetimeDeserializer

最后可以在 RestTemplateConfig 类中对常见调用问题进行汇总处理,可以参考如下:

package com.power.demo.restclient.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.collect.Lists;
import com.power.demo.util.DateTimeSerializer;
import com.power.demo.util.DatetimeDeserializer;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Component
public class RestTemplateConfig {

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例</span>

@Autowired
private RestTemplateBuilder builder;

@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ObjectMapper objectMapper;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例</span>

@Bean
public RestTemplate restTemplate() {

    RestTemplate restTemplate </span>=<span style="color: rgba(0, 0, 0, 1)"> builder.build();

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">注册model,用于实现jackson joda time序列化和反序列化</span>
    SimpleModule module = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> SimpleModule();
    module.addSerializer(DateTime.</span><span style="color: rgba(0, 0, 255, 1)">class</span>, <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> DateTimeSerializer());
    module.addDeserializer(DateTime.</span><span style="color: rgba(0, 0, 255, 1)">class</span>, <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> DatetimeDeserializer());
    objectMapper.registerModule(module);

    List</span>&lt;HttpMessageConverter&lt;?&gt;&gt; messageConverters =<span style="color: rgba(0, 0, 0, 1)"> Lists.newArrayList();
    MappingJackson2HttpMessageConverter converter </span>= <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MappingJackson2HttpMessageConverter();
    converter.setObjectMapper(objectMapper);

    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">不加会出现异常
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">Could not extract response: no suitable HttpMessageConverter found for response type [class ]</span>
    MediaType[] mediaTypes = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> MediaType[]{
            MediaType.APPLICATION_JSON,
            MediaType.APPLICATION_OCTET_STREAM,

            MediaType.TEXT_HTML,
            MediaType.TEXT_PLAIN,
            MediaType.TEXT_XML,
            MediaType.APPLICATION_STREAM_JSON,
            MediaType.APPLICATION_ATOM_XML,
            MediaType.APPLICATION_FORM_URLENCODED,
            MediaType.APPLICATION_JSON_UTF8,
            MediaType.APPLICATION_PDF,
    };

    converter.setSupportedMediaTypes(Arrays.asList(mediaTypes));

    </span><span style="color: rgba(0, 0, 255, 1)">try</span><span style="color: rgba(0, 0, 0, 1)"> {
        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">通过反射设置MessageConverters</span>
        Field field = restTemplate.getClass().getDeclaredField("messageConverters"<span style="color: rgba(0, 0, 0, 1)">);

        field.setAccessible(</span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">);

        List</span>&lt;HttpMessageConverter&lt;?&gt;&gt; orgConverterList = (List&lt;HttpMessageConverter&lt;?&gt;&gt;<span style="color: rgba(0, 0, 0, 1)">) field.get(restTemplate);

        Optional</span>&lt;HttpMessageConverter&lt;?&gt;&gt; opConverter =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream()
                .filter(x </span>-&gt; x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">
                        .getName()))
                .findFirst();

        </span><span style="color: rgba(0, 0, 255, 1)">if</span> (opConverter.isPresent() == <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">) {
            </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate;
        }

        messageConverters.add(converter);</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加MappingJackson2HttpMessageConverter

        </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加原有的剩余的HttpMessageConverter</span>
        List&lt;HttpMessageConverter&lt;?&gt;&gt; leftConverters =<span style="color: rgba(0, 0, 0, 1)"> orgConverterList.stream()
                .filter(x </span>-&gt; x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.<span style="color: rgba(0, 0, 255, 1)">class</span><span style="color: rgba(0, 0, 0, 1)">
                        .getName()) </span>== <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">)
                .collect(Collectors.toList());

        messageConverters.addAll(leftConverters);

        System.out.println(String.format(</span>"【HttpMessageConverter】原有数量:%s,重新构造后数量:%s"<span style="color: rgba(0, 0, 0, 1)">
                , orgConverterList.size(), messageConverters.size()));

    } </span><span style="color: rgba(0, 0, 255, 1)">catch</span><span style="color: rgba(0, 0, 0, 1)"> (Exception e) {
        e.printStackTrace();
    }

    restTemplate.setMessageConverters(messageConverters);

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> restTemplate;
}

}

RestTemplateConfig

目前良好地解决了 RestTemplate 常见调用问题,而且不需要你写 RestTemplate 帮助工具类了。

上面列举的这些常见问题,其实.NET 下面也有,有兴趣大家可以搜索一下微软的 HttpClient 常见使用问题,用过的人都深有体会。更不用提RestSharp这个开源类库,几年前用的过程中发现了非常多的 Bug,到现在还有一个反序列化数组的问题困扰着我们,我只好自己造个简单轮子特殊处理,给我最深刻的经验就是,很多看上去简单的功能,真的碰到了依然会花掉不少的时间去排查和解决,甚至要翻看源码。所以,我们写代码要认识到,越是通用的工具,越需要考虑到特例,可能你需要花 80% 以上的精力去处理 20% 的特殊情况,这估计也是满足常见的二八定律吧。

 

参考:

https://stackoverflow.com/questions/21854369/no-suitable-httpmessageconverter-found-for-response-type

https://stackoverflow.com/questions/40726145/rest-templatecould-not-extract-response-no-suitable-httpmessageconverter-found

https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter

http://www.cnblogs.com/rollenholt/p/3934649.html

http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found