咱们能够经过Spring Boot快速开发REST接口,同时也可能须要在实现接口的过程当中,经过Spring Boot调用内外部REST接口完成业务逻辑。html
在Spring Boot中,调用REST Api常见的通常主要有两种方式,经过自带的RestTemplate或者本身开发http客户端工具实现服务调用。java
RestTemplate基本功能很是强大,不过某些特殊场景,咱们可能仍是更习惯用本身封装的工具类,好比上传文件至分布式文件系统、处理带证书的https请求等。git
本文以RestTemplate来举例,记录几个使用RestTemplate调用接口过程当中发现的问题和解决方案。github
咱们本身封装的HttpClient,一般都会有一些模板代码,好比创建链接,构造请求头和请求体,而后根据响应,解析响应信息,最后关闭链接。web
RestTemplate是Spring中对HttpClient的再次封装,简化了发起HTTP请求以及处理响应的过程,抽象层级更高,减小消费者的模板代码,使冗余代码更少。spring
其实仔细想一想Spring Boot下的不少XXXTemplate类,它们也提供各类模板方法,只不过抽象的层次更高,隐藏了更多细节而已。json
顺便提一下,Spring Cloud有一个声明式服务调用Feign,是基于Netflix Feign实现的,整合了Spring Cloud Ribbon与 Spring Cloud Hystrix,而且实现了声明式的Web服务客户端定义方式。api
本质上Feign是在RestTemplate的基础上对其再次封装,由它来帮助咱们定义和实现依赖服务接口的定义。数组
常见的REST服务有不少种请求方式,如GET,POST,PUT,DELETE,HEAD,OPTIONS等。RestTemplate实现了最多见的方式,用的最多的就是Get和Post了,调用API可参考源码,这里列举几个方法定义(GET、POST、DELETE):app
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)
同时要注意两个较为“灵活”的方法exchange和execute。
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方法的实现以下:
doExecute方法封装了模板方法,好比建立链接、处理请求和应答,关闭链接等。
多数人看到这里,估计都会以为封装一个RestClient不过如此吧?
以一个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 = null; try { response = restTemplate.postForObject(svcUrl, request, ClientGetGoodsByGoodsIdResponse.class); } catch (Exception e) { e.printStackTrace(); response = new ClientGetGoodsByGoodsIdResponse(); response.setCode(AppConst.FAIL); response.setMessage(e.toString()); } return response; } private String getGoodsSvcUrl() { String url = ""; if (_serviceUrl == null) { _serviceUrl = ""; } if (_serviceUrl.length() == 0) { return url; } if (_serviceUrl.substring(_serviceUrl.length() - 1, _serviceUrl.length()) == "/") { url = String.format("%sapi/v1/goods", _serviceUrl); } else { url = String.format("%s/api/v1/goods", _serviceUrl); } return url; } }
demo里直接RestTemplate.postForObject方法调用,反序列化实体转换这些RestTemplate内部封装搞定。
这个问题一般会出如今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); return; } } else if (messageConverter.canWrite(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 + "]"); } } ((HttpMessageConverter<Object>) messageConverter).write( requestBody, requestContentType, httpRequest); return; } } String message = "Could not write request: no suitable HttpMessageConverter found for request type [" + requestBodyClass.getName() + "]"; if (requestContentType != null) { message += " and content type [" + requestContentType + "]"; } throw new RestClientException(message); } }
最简单的解决方案是,能够经过包装http请求头,并将请求对象序列化成字符串的形式传参,参考示例代码以下:
若是咱们还想直接返回对象,直接反序列化返回的字符串便可:
/* * Post请求调用 * */ public static <T> T postForObject(RestTemplate restTemplate, String url, Object params, Class<T> clazz) { T response = null; String respStr = postForObject(restTemplate, url, params); response = SerializeUtil.DeSerialize(respStr, clazz); return response; }
其中,序列化和反序列化工具比较多,经常使用的好比fastjson、jackson和gson。
和发起请求发生异常同样,处理应答的时候也会有问题。
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:
可是有些应用接口默认的应答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); try { for (HttpMessageConverter<?> messageConverter : this.messageConverters) { if (messageConverter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericMessageConverter = (GenericHttpMessageConverter<?>) messageConverter; if (genericMessageConverter.canRead(this.responseType, null, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Reading [" + this.responseType + "] as \"" + contentType + "\" using [" + messageConverter + "]"); } return (T) genericMessageConverter.read(this.responseType, null, responseWrapper); } } if (this.responseClass != null) { if (messageConverter.canRead(this.responseClass, contentType)) { if (logger.isDebugEnabled()) { logger.debug("Reading [" + this.responseClass.getName() + "] as \"" + contentType + "\" using [" + messageConverter + "]"); } return (T) messageConverter.read((Class) this.responseClass, responseWrapper); } } } } catch (IOException | HttpMessageNotReadableException ex) { throw new RestClientException("Error while extracting response for type [" + this.responseType + "] and content type [" + contentType + "]", ex); } throw new RestClientException("Could not extract response: no suitable HttpMessageConverter found " + "for response type [" + this.responseType + "] and content type [" + contentType + "]"); }
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 { private static final boolean romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", RestTemplate .class.getClassLoader()); private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader()); private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.class.getClassLoader()); private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.class.getClassLoader()); private static final boolean jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", RestTemplate.class.getClassLoader()); private static final boolean jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", RestTemplate.class.getClassLoader()); private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.class.getClassLoader()); private static final boolean jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", RestTemplate.class.getClassLoader()); // 启动的时候要注意,因为咱们在服务中注入了RestTemplate,因此启动的时候须要实例化该类的一个实例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例 @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = builder.build(); List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //不加会出现异常 //Could not extract response: no suitable HttpMessageConverter found for response type [class ] 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)); //messageConverters.add(converter); if (jackson2Present) { messageConverters.add(converter); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } else if (jsonbPresent) { messageConverters.add(new JsonbHttpMessageConverter()); } messageConverters.add(new FormHttpMessageConverter()); messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(new StringHttpMessageConverter()); messageConverters.add(new ResourceHttpMessageConverter(false)); messageConverters.add(new SourceHttpMessageConverter()); messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (jackson2XmlPresent) { messageConverters.add(new MappingJackson2XmlHttpMessageConverter()); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2SmilePresent) { messageConverters.add(new MappingJackson2SmileHttpMessageConverter()); } if (jackson2CborPresent) { messageConverters.add(new MappingJackson2CborHttpMessageConverter()); } restTemplate.setMessageConverters(messageConverters); return restTemplate; } }
看到上面的代码,再对比一下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 { // 启动的时候要注意,因为咱们在服务中注入了RestTemplate,因此启动的时候须要实例化该类的一个实例 @Autowired private RestTemplateBuilder builder; @Autowired private ObjectMapper objectMapper; // 使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例 @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = builder.build(); List<HttpMessageConverter<?>> messageConverters = Lists.newArrayList(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //不加可能会出现异常 //Could not extract response: no suitable HttpMessageConverter found for response type [class ] 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)); try { //经过反射设置MessageConverters Field field = restTemplate.getClass().getDeclaredField("messageConverters"); field.setAccessible(true); List<HttpMessageConverter<?>> orgConverterList = (List<HttpMessageConverter<?>>) field.get(restTemplate); Optional<HttpMessageConverter<?>> opConverter = orgConverterList.stream() .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class .getName())) .findFirst(); if (opConverter.isPresent() == false) { return restTemplate; } messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter //添加原有的剩余的HttpMessageConverter List<HttpMessageConverter<?>> leftConverters = orgConverterList.stream() .filter(x -> x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class .getName()) == false) .collect(Collectors.toList()); messageConverters.addAll(leftConverters); System.out.println(String.format("【HttpMessageConverter】原有数量:%s,从新构造后数量:%s" , orgConverterList.size(), messageConverters.size())); } catch (Exception e) { e.printStackTrace(); } restTemplate.setMessageConverters(messageConverters); return restTemplate; } }
除了一个messageConverters字段,看上去咱们再也不关心RestTemplate那些外部依赖包和内部构造过程,果真干净简洁好维护了不少。
这个也是一个很是经典的问题。解决方案很是简单,找到HttpMessageConverter,看看默认支持的Charset。AbstractJackson2HttpMessageConverter是不少HttpMessageConverter的基类,默认编码为UTF-8:
而StringHttpMessageConverter比较特殊,有人反馈过发生乱码问题由它默认支持的编码ISO-8859-1引发:
若是在使用过程当中发生乱码,咱们能够经过方法设置HttpMessageConverter支持的编码,经常使用的有UTF-八、GBK等。
这是开发过程当中容易碰到的又一个问题。由于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自定义序列化:
以及DateTime反序列化:
最后能够在RestTemplateConfig类中对常见调用问题进行汇总处理,能够参考以下:
目前良好地解决了RestTemplate常见调用问题,并且不须要你写RestTemplate帮助工具类了。
上面列举的这些常见问题,其实.NET下面也有,有兴趣你们能够搜索一下微软的HttpClient常见使用问题,用过的人都深有体会。更不用提RestSharp这个开源类库,几年前用的过程当中发现了很是多的Bug,到如今还有一个反序列化数组的问题困扰着咱们,我只好本身造个简单轮子特殊处理,给我最深入的经验就是,不少看上去简单的功能,真的碰到了依然会花掉很多的时间去排查和解决,甚至要翻看源码。因此,咱们写代码要认识到,越是通用的工具,越须要考虑到特例,可能你须要花80%以上的精力去处理20%的特殊状况,这估计也是知足常见的二八定律吧。
参考: