Spring Boot使用RestTemplate消费REST服务的几个问题记录
我们可以通过SpringBoot快速开发REST接口,同时也可能需要在实现接口的过程中,通过SpringBoot调用内外部REST接口完成业务逻辑。
在SpringBoot中,调用RESTApi常见的一般主要有两种方式,通过自带的RestTemplate或者自己开发http客户端工具实现服务调用。
RestTemplate基本功能非常强大,不过某些特殊场景,我们可能还是更习惯用自己封装的工具类,比如上传文件至分布式文件系统、处理带证书的https请求等。
本文以RestTemplate来举例,记录几个使用RestTemplate调用接口过程中发现的问题和解决方案。
一、RestTemplate简介
1、什么是RestTemplate
我们自己封装的HttpClient,通常都会有一些模板代码,比如建立连接,构造请求头和请求体,然后根据响应,解析响应信息,最后关闭连接。
RestTemplate是Spring中对HttpClient的再次封装,简化了发起HTTP请求以及处理响应的过程,抽象层级更高,减少消费者的模板代码,使冗余代码更少。
其实仔细想想SpringBoot下的很多XXXTemplate类,它们也提供各种模板方法,只不过抽象的层次更高,隐藏了更多细节而已。
顺便提一下,SpringCloud有一个声明式服务调用Feign,是基于NetflixFeign实现的,整合了SpringCloudRibbon与SpringCloudHystrix,并且实现了声明式的Web服务客户端定义方式。
本质上Feign是在RestTemplate的基础上对其再次封装,由它来帮助我们定义和实现依赖服务接口的定义。
2、RestTemplate常见方法
常见的REST服务有很多种请求方式,如GET,POST,PUT,DELETE,HEAD,OPTIONS等。RestTemplate实现了最常见的方式,用的最多的就是Get和Post了,调用API可参考源码,这里列举几个方法定义(GET、POST、DELETE):
methods
publicTgetForObject(Stringurl,Class responseType,Object...uriVariables) public ResponseEntity getForEntity(Stringurl,Class responseType,Object...uriVariables) public TpostForObject(Stringurl,@NullableObjectrequest,Class responseType,Object...uriVariables) public ResponseEntity postForEntity(Stringurl,@NullableObjectrequest,Class responseType,Object...uriVariables) publicvoiddelete(Stringurl,Object...uriVariables) publicvoiddelete(URIurl)
同时要注意两个较为“灵活”的方法exchange和execute。
RestTemplate暴露的exchange与其它接口的不同:
(1)允许调用者指定HTTP请求的方法(GET,POST,DELETE等)
(2)可以在请求中增加body以及头信息,其内容通过参数‘HttpEntity>requestEntity'描述
(3)exchange支持‘含参数的类型'(即泛型类)作为返回类型,该特性通过‘ParameterizedTypeReference
RestTemplate所有的GET,POST等等方法,最终调用的都是execute方法。excute方法的内部实现是将String格式的URI转成了java.net.URI,之后调用了doExecute方法,doExecute方法的实现如下:
doExecute
/** *ExecutethegivenmethodontheprovidedURI. *The{@linkClientHttpRequest}isprocessedusingthe{@linkRequestCallback}; *theresponsewiththe{@linkResponseExtractor}. *@paramurlthefully-expandedURLtoconnectto *@parammethodtheHTTPmethodtoexecute(GET,POST,etc.) *@paramrequestCallbackobjectthatpreparestherequest(canbe{@codenull}) *@paramresponseExtractorobjectthatextractsthereturnvaluefromtheresponse(canbe{@codenull}) *@returnanarbitraryobject,asreturnedbythe{@linkResponseExtractor} */ @Nullable protected
TdoExecute(URIurl,@NullableHttpMethodmethod,@NullableRequestCallbackrequestCallback, @NullableResponseExtractor responseExtractor)throwsRestClientException{ Assert.notNull(url,"'url'mustnotbenull"); Assert.notNull(method,"'method'mustnotbenull"); ClientHttpResponseresponse=null; try{ ClientHttpRequestrequest=createRequest(url,method); if(requestCallback!=null){ requestCallback.doWithRequest(request); } response=request.execute(); handleResponse(url,method,response); if(responseExtractor!=null){ returnresponseExtractor.extractData(response); } else{ returnnull; } } catch(IOExceptionex){ Stringresource=url.toString(); Stringquery=url.getRawQuery(); resource=(query!=null?resource.substring(0,resource.indexOf('?')):resource); thrownewResourceAccessException("I/Oerroron"+method.name()+ "requestfor\""+resource+"\":"+ex.getMessage(),ex); } finally{ if(response!=null){ response.close(); } } }
doExecute方法封装了模板方法,比如创建连接、处理请求和应答,关闭连接等。
多数人看到这里,估计都会觉得封装一个RestClient不过如此吧?
3、简单调用
以一个POST调用为例:
GoodsServiceClient
packagecom.power.demo.restclient; importcom.power.demo.common.AppConst; importcom.power.demo.restclient.clientrequest.ClientGetGoodsByGoodsIdRequest; importcom.power.demo.restclient.clientresponse.ClientGetGoodsByGoodsIdResponse; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.beans.factory.annotation.Value; importorg.springframework.stereotype.Component; importorg.springframework.web.client.RestTemplate; /** *商品REST接口客户端(demo测试用) **/ @Component publicclassGoodsServiceClient{ //服务消费者调用的接口URL形如:http://localhost:9090 @Value("${spring.power.serviceurl}") privateString_serviceUrl; @Autowired privateRestTemplaterestTemplate; publicClientGetGoodsByGoodsIdResponsegetGoodsByGoodsId(ClientGetGoodsByGoodsIdRequestrequest){ StringsvcUrl=getGoodsSvcUrl()+"/getinfobyid"; ClientGetGoodsByGoodsIdResponseresponse=null; try{ response=restTemplate.postForObject(svcUrl,request,ClientGetGoodsByGoodsIdResponse.class); }catch(Exceptione){ e.printStackTrace(); response=newClientGetGoodsByGoodsIdResponse(); response.setCode(AppConst.FAIL); response.setMessage(e.toString()); } returnresponse; } privateStringgetGoodsSvcUrl(){ Stringurl=""; if(_serviceUrl==null){ _serviceUrl=""; } if(_serviceUrl.length()==0){ returnurl; } if(_serviceUrl.substring(_serviceUrl.length()-1,_serviceUrl.length())=="/"){ url=String.format("%sapi/v1/goods",_serviceUrl); }else{ url=String.format("%s/api/v1/goods",_serviceUrl); } returnurl; } }
demo里直接RestTemplate.postForObject方法调用,反序列化实体转换这些RestTemplate内部封装搞定。
二、问题汇总
1、nosuitableHttpMessageConverterfoundforrequesttype异常
这个问题通常会出现在postForObject中传入对象进行调用的时候。
分析RestTemplate源码,在HttpEntityRequestCallback类的doWithRequest方法中,如果messageConverters(这个字段后面会继续提及)列表字段循环处理的过程中没有满足return跳出的逻辑(也就是没有匹配的HttpMessageConverter),则抛出上述异常:
HttpEntityRequestCallback.doWithRequest
@Override @SuppressWarnings("unchecked") publicvoiddoWithRequest(ClientHttpRequesthttpRequest)throwsIOException{ super.doWithRequest(httpRequest); ObjectrequestBody=this.requestEntity.getBody(); if(requestBody==null){ HttpHeadershttpHeaders=httpRequest.getHeaders(); HttpHeadersrequestHeaders=this.requestEntity.getHeaders(); if(!requestHeaders.isEmpty()){ for(Map.Entry>entry:requestHeaders.entrySet()){ httpHeaders.put(entry.getKey(),newLinkedList<>(entry.getValue())); } } if(httpHeaders.getContentLength()<0){ httpHeaders.setContentLength(0L); } } else{ Class>requestBodyClass=requestBody.getClass(); TyperequestBodyType=(this.requestEntityinstanceofRequestEntity? ((RequestEntity>)this.requestEntity).getType():requestBodyClass); HttpHeadershttpHeaders=httpRequest.getHeaders(); HttpHeadersrequestHeaders=this.requestEntity.getHeaders(); MediaTyperequestContentType=requestHeaders.getContentType(); for(HttpMessageConverter>messageConverter:getMessageConverters()){ if(messageConverterinstanceofGenericHttpMessageConverter){ GenericHttpMessageConverter
最简单的解决方案是,可以通过包装http请求头,并将请求对象序列化成字符串的形式传参,参考示例代码如下:
postForObject
/* *Post请求调用 **/ publicstaticStringpostForObject(RestTemplaterestTemplate,Stringurl,Objectparams){ HttpHeadersheaders=newHttpHeaders(); MediaTypetype=MediaType.parseMediaType("application/json;charset=UTF-8"); headers.setContentType(type); headers.add("Accept",MediaType.APPLICATION_JSON.toString()); Stringjson=SerializeUtil.Serialize(params); HttpEntityformEntity=newHttpEntity (json,headers); Stringresult=restTemplate.postForObject(url,formEntity,String.class); returnresult; }
如果我们还想直接返回对象,直接反序列化返回的字符串即可:
postForObject
/* *Post请求调用 **/ publicstaticTpostForObject(RestTemplaterestTemplate,Stringurl,Objectparams,Class clazz){ Tresponse=null; StringrespStr=postForObject(restTemplate,url,params); response=SerializeUtil.DeSerialize(respStr,clazz); returnresponse; }
其中,序列化和反序列化工具比较多,常用的比如fastjson、jackson和gson。
2、nosuitableHttpMessageConverterfoundforresponsetype异常
和发起请求发生异常一样,处理应答的时候也会有问题。
StackOverflow上有人问过相同的问题,根本原因是HTTP消息转换器HttpMessageConverter缺少MIMEType,也就是说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:
MappingJackson2HttpMessageConverter
/** *Constructanew{@linkMappingJackson2HttpMessageConverter}withacustom{@linkObjectMapper}. *Youcanuse{@linkJackson2ObjectMapperBuilder}tobuilditeasily. *@seeJackson2ObjectMapperBuilder#json() */ publicMappingJackson2HttpMessageConverter(ObjectMapperobjectMapper){ super(objectMapper,MediaType.APPLICATION_JSON,newMediaType("application","*+json")); }
但是有些应用接口默认的应答MIMEType不是application/json,比如我们调用一个外部天气预报接口,如果使用RestTemplate的默认配置,直接返回一个字符串应答是没有问题的:
Stringurl="http://wthrcdn.etouch.cn/weather_mini?city=上海"; Stringresult=restTemplate.getForObject(url,String.class); ClientWeatherResultVOvo=SerializeUtil.DeSerialize(result,ClientWeatherResultVO.class);
但是,如果我们想直接返回一个实体对象:
Stringurl="http://wthrcdn.etouch.cn/weather_mini?city=上海"; ClientWeatherResultVOweatherResultVO=restTemplate.getForObject(url,ClientWeatherResultVO.class);
则直接报异常:
Couldnotextractresponse:nosuitableHttpMessageConverterfoundforresponsetype[class]
andcontenttype[application/octet-stream]
很多人碰到过这个问题,首次碰到估计大多都比较懵吧,很多接口都是json或者xml或者plaintext格式返回的,什么是application/octet-stream?
查看RestTemplate源代码,一路跟踪下去会发现HttpMessageConverterExtractor类的extractData方法有个解析应答及反序列化逻辑,如果不成功,抛出的异常信息和上述一致:
HttpMessageConverterExtractor.extractData
@Override @SuppressWarnings({"unchecked","rawtypes","resource"}) publicTextractData(ClientHttpResponseresponse)throwsIOException{ MessageBodyClientHttpResponseWrapperresponseWrapper=newMessageBodyClientHttpResponseWrapper(response); if(!responseWrapper.hasMessageBody()||responseWrapper.hasEmptyMessageBody()){ returnnull; } MediaTypecontentType=getContentType(responseWrapper); try{ for(HttpMessageConverter>messageConverter:this.messageConverters){ if(messageConverterinstanceofGenericHttpMessageConverter){ 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|HttpMessageNotReadableExceptionex){ thrownewRestClientException("Errorwhileextractingresponsefortype["+ this.responseType+"]andcontenttype["+contentType+"]",ex); } thrownewRestClientException("Couldnotextractresponse:nosuitableHttpMessageConverterfound"+ "forresponsetype["+this.responseType+"]andcontenttype["+contentType+"]"); }
StackOverflow上的解决的示例代码可以接受,但是并不准确,常见的MIMEType都应该加进去,贴一下我认为正确的代码:
RestTemplateConfig
packagecom.power.demo.restclient.config; importcom.fasterxml.jackson.databind.ObjectMapper; importcom.google.common.collect.Lists; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.boot.web.client.RestTemplateBuilder; importorg.springframework.context.annotation.Bean; importorg.springframework.http.MediaType; importorg.springframework.http.converter.*; importorg.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; importorg.springframework.http.converter.feed.AtomFeedHttpMessageConverter; importorg.springframework.http.converter.feed.RssChannelHttpMessageConverter; importorg.springframework.http.converter.json.GsonHttpMessageConverter; importorg.springframework.http.converter.json.JsonbHttpMessageConverter; importorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter; importorg.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; importorg.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; importorg.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; importorg.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; importorg.springframework.http.converter.xml.SourceHttpMessageConverter; importorg.springframework.stereotype.Component; importorg.springframework.util.ClassUtils; importorg.springframework.web.client.RestTemplate; importjava.util.Arrays; importjava.util.List; @Component publicclassRestTemplateConfig{ privatestaticfinalbooleanromePresent=ClassUtils.isPresent("com.rometools.rome.feed.WireFeed",RestTemplate .class.getClassLoader()); privatestaticfinalbooleanjaxb2Present=ClassUtils.isPresent("javax.xml.bind.Binder",RestTemplate.class.getClassLoader()); privatestaticfinalbooleanjackson2Present=ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",RestTemplate.class.getClassLoader())&&ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator",RestTemplate.class.getClassLoader()); privatestaticfinalbooleanjackson2XmlPresent=ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper",RestTemplate.class.getClassLoader()); privatestaticfinalbooleanjackson2SmilePresent=ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory",RestTemplate.class.getClassLoader()); privatestaticfinalbooleanjackson2CborPresent=ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory",RestTemplate.class.getClassLoader()); privatestaticfinalbooleangsonPresent=ClassUtils.isPresent("com.google.gson.Gson",RestTemplate.class.getClassLoader()); privatestaticfinalbooleanjsonbPresent=ClassUtils.isPresent("javax.json.bind.Jsonb",RestTemplate.class.getClassLoader()); //启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例 @Autowired privateRestTemplateBuilderbuilder; @Autowired privateObjectMapperobjectMapper; //使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例 @Bean publicRestTemplaterestTemplate(){ RestTemplaterestTemplate=builder.build(); List>messageConverters=Lists.newArrayList(); MappingJackson2HttpMessageConverterconverter=newMappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //不加会出现异常 //Couldnotextractresponse:nosuitableHttpMessageConverterfoundforresponsetype[class] MediaType[]mediaTypes=newMediaType[]{ 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); }elseif(gsonPresent){ messageConverters.add(newGsonHttpMessageConverter()); }elseif(jsonbPresent){ messageConverters.add(newJsonbHttpMessageConverter()); } messageConverters.add(newFormHttpMessageConverter()); messageConverters.add(newByteArrayHttpMessageConverter()); messageConverters.add(newStringHttpMessageConverter()); messageConverters.add(newResourceHttpMessageConverter(false)); messageConverters.add(newSourceHttpMessageConverter()); messageConverters.add(newAllEncompassingFormHttpMessageConverter()); if(romePresent){ messageConverters.add(newAtomFeedHttpMessageConverter()); messageConverters.add(newRssChannelHttpMessageConverter()); } if(jackson2XmlPresent){ messageConverters.add(newMappingJackson2XmlHttpMessageConverter()); }elseif(jaxb2Present){ messageConverters.add(newJaxb2RootElementHttpMessageConverter()); } if(jackson2SmilePresent){ messageConverters.add(newMappingJackson2SmileHttpMessageConverter()); } if(jackson2CborPresent){ messageConverters.add(newMappingJackson2CborHttpMessageConverter()); } restTemplate.setMessageConverters(messageConverters); returnrestTemplate; } }
看到上面的代码,再对比一下RestTemplate内部实现,就知道我参考了RestTemplate的源码,有洁癖的人可能会说这一坨代码有点啰嗦,上面那一堆staticfinal的变量和messageConverters填充数据方法,暴露了RestTemplate的实现,如果RestTemplate修改了,这里也要改,非常不友好,而且看上去一点也不OO。
经过分析,RestTemplateBuilder.build()构造了RestTemplate对象,只要将内部MappingJackson2HttpMessageConverter修改一下支持的MediaType即可,RestTemplate的messageConverters字段虽然是privatefinal的,我们依然可以通过反射修改之,改进后的代码如下:
RestTemplateConfig
packagecom.power.demo.restclient.config; importcom.fasterxml.jackson.databind.ObjectMapper; importcom.google.common.collect.Lists; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.boot.web.client.RestTemplateBuilder; importorg.springframework.context.annotation.Bean; importorg.springframework.http.MediaType; importorg.springframework.http.converter.HttpMessageConverter; importorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter; importorg.springframework.stereotype.Component; importorg.springframework.web.client.RestTemplate; importjava.lang.reflect.Field; importjava.util.Arrays; importjava.util.List; importjava.util.Optional; importjava.util.stream.Collectors; @Component publicclassRestTemplateConfig{ //启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例 @Autowired privateRestTemplateBuilderbuilder; @Autowired privateObjectMapperobjectMapper; //使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例 @Bean publicRestTemplaterestTemplate(){ RestTemplaterestTemplate=builder.build(); List>messageConverters=Lists.newArrayList(); MappingJackson2HttpMessageConverterconverter=newMappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //不加可能会出现异常 //Couldnotextractresponse:nosuitableHttpMessageConverterfoundforresponsetype[class] MediaType[]mediaTypes=newMediaType[]{ 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 Fieldfield=restTemplate.getClass().getDeclaredField("messageConverters"); field.setAccessible(true); List >orgConverterList=(List >)field.get(restTemplate); Optional >opConverter=orgConverterList.stream() .filter(x->x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class .getName())) .findFirst(); if(opConverter.isPresent()==false){ returnrestTemplate; } messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter //添加原有的剩余的HttpMessageConverter List >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(Exceptione){ e.printStackTrace(); } restTemplate.setMessageConverters(messageConverters); returnrestTemplate; } }
除了一个messageConverters字段,看上去我们不再关心RestTemplate那些外部依赖包和内部构造过程,果然干净简洁好维护了很多。
3、乱码问题
这个也是一个非常经典的问题。解决方案非常简单,找到HttpMessageConverter,看看默认支持的Charset。AbstractJackson2HttpMessageConverter是很多HttpMessageConverter的基类,默认编码为UTF-8:
AbstractJackson2HttpMessageConverter
publicabstractclassAbstractJackson2HttpMessageConverterextendsAbstractGenericHttpMessageConverter{ publicstaticfinalCharsetDEFAULT_CHARSET=StandardCharsets.UTF_8; }
而StringHttpMessageConverter比较特殊,有人反馈过发生乱码问题由它默认支持的编码ISO-8859-1引起:
StringHttpMessageConverter
/** *Implementationof{@linkHttpMessageConverter}thatcanreadandwritestrings. * *Bydefault,thisconvertersupportsallmediatypes({@code}), *andwriteswitha{@codeContent-Type}of{@codetext/plain}.Thiscanbeoverridden *bysettingthe{@link#setSupportedMediaTypessupportedMediaTypes}property. * *@authorArjenPoutsma *@authorJuergenHoeller *@since3.0 */ publicclassStringHttpMessageConverterextendsAbstractHttpMessageConverter
{ publicstaticfinalCharsetDEFAULT_CHARSET=StandardCharsets.ISO_8859_1; /** *Adefaultconstructorthatuses{@code"ISO-8859-1"}asthedefaultcharset. *@see#StringHttpMessageConverter(Charset) */ publicStringHttpMessageConverter(){ this(DEFAULT_CHARSET); } }
如果在使用过程中发生乱码,我们可以通过方法设置HttpMessageConverter支持的编码,常用的有UTF-8、GBK等。
4、反序列化异常
这是开发过程中容易碰到的又一个问题。因为Java的开源框架和工具类非常之多,而且版本更迭频繁,所以经常发生一些意想不到的坑。
以jodatime为例,jodatime是流行的java时间和日期框架,但是如果你的接口对外暴露jodatime的类型,比如DateTime,那么接口调用方(同构和异构系统)可能会碰到序列化难题,反序列化时甚至直接抛出如下异常:
org.springframework.http.converter.HttpMessageConversionException:Typedefinitionerror:[simpletype,classorg.joda.time.Chronology];nestedexceptioniscom.fasterxml.jackson.databind.exc.InvalidDefinitionException:Cannotconstructinstanceof`org.joda.time.Chronology`(noCreators,likedefaultconstruct,exist):abstracttypeseitherneedtobemappedtoconcretetypes,havecustomdeserializer,orcontainadditionaltypeinformation
at[Source:(PushbackInputStream);
我在前厂就碰到过,后来为了调用方便,改回直接暴露Java的Date类型。
当然解决的方案不止这一种,可以使用jackson支持自定义类的序列化和反序列化的方式。在精度要求不是很高的系统里,实现简单的DateTime自定义序列化:
DateTimeSerializer
packagecom.power.demo.util; importcom.fasterxml.jackson.core.JsonGenerator; importcom.fasterxml.jackson.core.JsonProcessingException; importcom.fasterxml.jackson.databind.JsonSerializer; importcom.fasterxml.jackson.databind.SerializerProvider; importorg.joda.time.DateTime; importorg.joda.time.format.DateTimeFormat; importorg.joda.time.format.DateTimeFormatter; importjava.io.IOException; /** *在默认情况下,jackson会将jodatime序列化为较为复杂的形式,不利于阅读,并且对象较大。 **JodaTime序列化的时候可以将datetime序列化为字符串,更容易读 **/ publicclassDateTimeSerializerextendsJsonSerializer
{ privatestaticDateTimeFormatterdateFormatter=DateTimeFormat.forPattern("yyyy-MM-ddHH:mm:ss"); @Override publicvoidserialize(DateTimevalue,JsonGeneratorjgen,SerializerProviderprovider)throwsIOException,JsonProcessingException{ jgen.writeString(value.toString(dateFormatter)); } }
以及DateTime反序列化:
DatetimeDeserializer
packagecom.power.demo.util; importcom.fasterxml.jackson.core.JsonParser; importcom.fasterxml.jackson.core.JsonProcessingException; importcom.fasterxml.jackson.databind.DeserializationContext; importcom.fasterxml.jackson.databind.JsonDeserializer; importcom.fasterxml.jackson.databind.JsonNode; importorg.joda.time.DateTime; importorg.joda.time.format.DateTimeFormat; importorg.joda.time.format.DateTimeFormatter; importjava.io.IOException; /** *JodaTime反序列化将字符串转化为datetime **/ publicclassDatetimeDeserializerextendsJsonDeserializer{ privatestaticDateTimeFormatterdateFormatter=DateTimeFormat.forPattern("yyyy-MM-ddHH:mm:ss"); @Override publicDateTimedeserialize(JsonParserjp,DeserializationContextcontext)throwsIOException,JsonProcessingException{ JsonNodenode=jp.getCodec().readTree(jp); Strings=node.asText(); DateTimeparse=DateTime.parse(s,dateFormatter); returnparse; } }
最后可以在RestTemplateConfig类中对常见调用问题进行汇总处理,可以参考如下:
RestTemplateConfig
packagecom.power.demo.restclient.config; importcom.fasterxml.jackson.databind.ObjectMapper; importcom.fasterxml.jackson.databind.module.SimpleModule; importcom.google.common.collect.Lists; importcom.power.demo.util.DateTimeSerializer; importcom.power.demo.util.DatetimeDeserializer; importorg.joda.time.DateTime; importorg.springframework.beans.factory.annotation.Autowired; importorg.springframework.boot.web.client.RestTemplateBuilder; importorg.springframework.context.annotation.Bean; importorg.springframework.http.MediaType; importorg.springframework.http.converter.HttpMessageConverter; importorg.springframework.http.converter.json.MappingJackson2HttpMessageConverter; importorg.springframework.stereotype.Component; importorg.springframework.web.client.RestTemplate; importjava.lang.reflect.Field; importjava.util.Arrays; importjava.util.List; importjava.util.Optional; importjava.util.stream.Collectors; @Component publicclassRestTemplateConfig{ //启动的时候要注意,由于我们在服务中注入了RestTemplate,所以启动的时候需要实例化该类的一个实例 @Autowired privateRestTemplateBuilderbuilder; @Autowired privateObjectMapperobjectMapper; //使用RestTemplateBuilder来实例化RestTemplate对象,spring默认已经注入了RestTemplateBuilder实例 @Bean publicRestTemplaterestTemplate(){ RestTemplaterestTemplate=builder.build(); //注册model,用于实现jacksonjodatime序列化和反序列化 SimpleModulemodule=newSimpleModule(); module.addSerializer(DateTime.class,newDateTimeSerializer()); module.addDeserializer(DateTime.class,newDatetimeDeserializer()); objectMapper.registerModule(module); List>messageConverters=Lists.newArrayList(); MappingJackson2HttpMessageConverterconverter=newMappingJackson2HttpMessageConverter(); converter.setObjectMapper(objectMapper); //不加会出现异常 //Couldnotextractresponse:nosuitableHttpMessageConverterfoundforresponsetype[class] MediaType[]mediaTypes=newMediaType[]{ 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 Fieldfield=restTemplate.getClass().getDeclaredField("messageConverters"); field.setAccessible(true); List >orgConverterList=(List >)field.get(restTemplate); Optional >opConverter=orgConverterList.stream() .filter(x->x.getClass().getName().equalsIgnoreCase(MappingJackson2HttpMessageConverter.class .getName())) .findFirst(); if(opConverter.isPresent()==false){ returnrestTemplate; } messageConverters.add(converter);//添加MappingJackson2HttpMessageConverter //添加原有的剩余的HttpMessageConverter List >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(Exceptione){ e.printStackTrace(); } restTemplate.setMessageConverters(messageConverters); returnrestTemplate; } }
目前良好地解决了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://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。