一、请求流程
在RestTemplet 请求中,请求发送一个 HTTP 请求时,RestTemplet 会根据请求中的内容类型(Content-Type)选择合适的 HttpMessageConverter 来处理请求体的数据。同样地,当服务器返回一个 HTTP 响应时,RestTemplet 会根据请求中的 Accept 头部信息选择合适的 HttpMessageConverter 来处理响应体的数据。
处理过程和SpringMVC的处理过程类似
二、spring RestTemplate自定义HttpMessageConverter
项目中通过RestTemplate调用其他接口,接口返回的Content-Type是text/json;charset=utf-8,导致调用时报错: Could not extract response: no suitable HttpMessageConverter found for response type [class java.lang.Object] and content type [text/json;charset=utf-8],我们可以通过自定义HttpMessageConverter来解决。
问题分析
我们使用RestTemplate一般会指定返回的数据转换成何种类型,比如如下使用方式,我们告诉RestTemplate将返回的数据转换成A.class的对象。
restTemplate.postForObject("http://localhost:8080/rt/testUrl", entity, A.class);
我们知道,http返回的数据只是一些二进制数据而已,如何将这些二进制数据转换成指定的类型?这就要用到HttpMessageConverter了。跟踪源码我们可以知道,RestTemplate是在org.springframework.web.client.HttpMessageConverterExtractor#extractData这个方法中对数据进行转换的。
为方便后面分析,先把这个方法的代码贴在这里。
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()) {ResolvableType resolvableType = ResolvableType.forType(this.responseType);logger.debug("Reading to [" + resolvableType + "]");}return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);}}if (this.responseClass != null) {if (messageConverter.canRead(this.responseClass, contentType)) {if (logger.isDebugEnabled()) {String className = this.responseClass.getName();logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");}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 UnknownContentTypeException(this.responseType, contentType,responseWrapper.getRawStatusCode(), responseWrapper.getStatusText(),responseWrapper.getHeaders(), getResponseBody(responseWrapper));}
这里的思路很清楚,先获取response的contentType,然后遍历所有HttpMessageConverter,调用HttpMessageConverter方法的canRead方法判断当前HttpMessageConvert能不能转换这个response,如果可以,就直接调用read方法,并返回结果。从这里也可以看出,HttpMessageConverter的顺序也很重要,一旦发现某个HttpMessageConverter可用,后面的就不会再判断了。
同时注意到,方法会先判断HttpMessageConverter是否是GenericHttpMessageConverter类型的,如果是,就调用canRead方法进行判断是否可以读取当前response。GenericHttpMessageConverter是HttpMessageConverter的子接口,可以支持泛型。我们平时用泛型还是挺多的,所以我们就通过实现GenericHttpMessageConverter来演示如果自定义HttpMessageConverter来支持ContentType为text/json的接口。
问题解决
先把测试代码写一下。
测试接口
@Controller
@RequestMapping("/rt")
public class RtTestController {@Autowiredprivate RestTemplate restTemplate;@ResponseBody@RequestMapping("test")public Object test() {HttpHeaders headers = new HttpHeaders();HttpEntity entity = new HttpEntity(null, headers);// 调用testUrl接口,testUrl接口返回的ContentType是text/jsonObject o = restTemplate.postForObject("http://localhost:8080/rt/testUrl", entity, Object.class);return o;}@RequestMapping("testUrl")public ResponseEntity testUrl() {HttpHeaders headers = new HttpHeaders();// 指定返回ContentType为text/jsonheaders.setContentType(MediaType.valueOf("text/json;charset=utf-8"));// 注意这里返回的数据为{},因为一会我们要用json工具解析数据,所以要返回符合json格式的数据。ResponseEntity entity = new ResponseEntity("{}", headers, HttpStatus.OK);return entity;}
}
配置RestTemplate
@Configuration
public class RestTemplateConfig {@Beanpublic RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();return restTemplate;}
}
三、通过实现GenericHttpMessageConverter自定义HttpMessageConverter
public class MyCustomGenericHttpMessageConverter implements GenericHttpMessageConverter<Object> {@Overridepublic boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) {// 如果MediaType是text/json类型,返回truereturn MediaType.valueOf("text/json").includes(mediaType);}@Overridepublic Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {// 利用json工具将数据转换成java对象,这里用的是hutool的json工具类。String content = IoUtil.read(inputMessage.getBody(), Charset.defaultCharset());return JSONUtil.toBean(content, type, false);}@Overridepublic boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {// 这里只演示read,所以canWrite返回false.return false;}@Overridepublic void write(Object o, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {}@Overridepublic boolean canRead(Class<?> clazz, MediaType mediaType) {// GenericHttpMessageConverter用的是canRead(Type type, Class<?> contextClass, MediaType mediaType)// 所以这个方法可以不用管return false;}@Overridepublic boolean canWrite(Class<?> clazz, MediaType mediaType) {return false;}@Overridepublic List<MediaType> getSupportedMediaTypes() {return null;}@Overridepublic Object read(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {return null;}@Overridepublic void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {}
}
将自定义的HttpMessageConverter添加到RestTemplate中
@Configuration
public class RestTemplateConfig {@Beanpublic RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();restTemplate.getMessageConverters().add(new MyCustomGenericHttpMessageConverter());return restTemplate;}
}
这样配置好后,再调用test接口就不会报错了。
四、通过继承AbstractGenericHttpMessageConverter自定义HttpMessageConverter
直接实现GenericHttpMessageConverter我们需要重写很多方法,spring给我们提供了一个更方便的抽象类AbstractGenericHttpMessageConverter,也可以通过继承这个类来自定义,代码如下。
public class MyCustomGenericHttpMessageConverter2 extends AbstractGenericHttpMessageConverter<Object> {// 通过构造函数指定支持的MediaTypepublic MyCustomGenericHttpMessageConverter2(MediaType mediaType) {super(mediaType);}@Overrideprotected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {// 这里只演示read}@Overrideprotected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {return null;}@Overridepublic Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {// 利用json工具将数据转换成java对象,这里用的是hutool的json工具类。String content = IoUtil.read(inputMessage.getBody(), Charset.defaultCharset());return JSONUtil.toBean(content, type, false);}
}
配置RestTemplate
@Configuration
public class RestTemplateConfig {@Beanpublic RestTemplate restTemplate() {RestTemplate restTemplate = new RestTemplate();MyCustomGenericHttpMessageConverter2 converter = new MyCustomGenericHttpMessageConverter2(MediaType.valueOf("text/json"));restTemplate.getMessageConverters().add(converter);return restTemplate;}
}
五、通过BeanPostProcessor扩展RestTemplate
这里再多说一点吧,由于我们开发使用的是公司的框架,框架中已经封装好了RestTemplate,这时再自定义一个RestTemplate就不合适了,我们可以通过BeanPostProcessor来扩展已有的RestTemplate,代码如下。
@Component
public class RestTemplateBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof RestTemplate) {RestTemplate restBean = (RestTemplate) bean;MyCustomGenericHttpMessageConverter2 converter = new MyCustomGenericHttpMessageConverter2(MediaType.valueOf("text/json"));restBean.getMessageConverters().add(converter);}return bean;}
}
六、Error while extracting response for type [] and content type [],json返回值被解析为xml
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/mofeimo110/article/details/122087117
在使用restTemplate请求restful接口时,在特定情况下总会将返回的json数据解析为xml数据然后处理,接着就会爆出标题中的错误:
Error while extracting response for type [] and content type [application/xml;charset=UTF-8]; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unexpected character '5' (code 53) in content after '<' (malformed start element?).at [row,col {unknown-source}]: [1,15395]; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Unexpected character '5' (code 53) in content after '<' (malformed start element?).
根据错误信息来看,似乎响应头标记了返回类型为[application/xml;charset=UTF-8],然而实际情况是所有的返回数据都是[application/json;charset=UTF-8]。
跟踪restTemplate源码,发现由new RestTemplate(httpRequestFactory())创建的实例会有7个converter:
继续跟踪restTemplate的exchange,当对response进行类型转换时,会迭代当前实例中所有的converter,然后选择一个支持当前类型的converter执行,使用canRead来判断:
此时就发现了问题,在特定情况下,响应头的contentType被读作了"application/xml",然而此时的真实数据仍然为json格式。所以将用于xml格式的converter删除,则迭代器会寻找下一个可执行的converter即MappingJackson2HttpMessageConverter。或者将二者顺序换一下,降低xml的优先级。
解决办法:
方案1:删除xml的转换器
@Beanpublic RestTemplate restTemplate() {RestTemplate template = new RestTemplate(httpRequestFactory());// 排除掉xml的解析converter,避免将json数据当做xml解析List<HttpMessageConverter<?>> collect = template.getMessageConverters().stream().filter(m -> !(m instanceof MappingJackson2XmlHttpMessageConverter)).collect(Collectors.toList());template.setMessageConverters(collect);return template;}
方案2:降低xml转换器优先级
@Beanpublic RestTemplate restTemplate() {RestTemplate template = new RestTemplate(httpRequestFactory());// 将xml解析的优先级调低int xml = 0, json = 0;List<HttpMessageConverter<?>> messageConverters = template.getMessageConverters();for (int i = 0; i < messageConverters.size(); i++) {HttpMessageConverter<?> h = messageConverters.get(i);if (h instanceof MappingJackson2XmlHttpMessageConverter) {xml = i;} else if (h instanceof MappingJackson2HttpMessageConverter) {json = i;}}Collections.swap(template.getMessageConverters(), xml, json);return template;}
一图搞定 RestTemplete HttpMessageConvert 消息解码 及 常见中文乱码问题
自定义HttpMessageConverter实现RestTemplate的exchange方法返回自定义格式数据
Spring/Boot/Cloud系列知识:HttpMessageConverter转换器使用方式