记一次RestTemplate消息类型不匹配的BUG定位
Posted AlaGeek
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次RestTemplate消息类型不匹配的BUG定位相关的知识,希望对你有一定的参考价值。
1、前因
由于跟第三方交互是用的XML协议,并且之前的代码用的很老的XML解析方法,解析效率不高,所以这次做需求的时候,打算使用Jackson来解析XML报文,因此在项目中加入了以下依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.3</version>
</dependency>
结果导致了RestTemplate发起请求抛出了异常,请求方式如下:
public Response execute(Request request)
Response response = null;
try
response = restTemplate.postForObject(url, request, Response.class);
catch (Exception e)
log.error("请求失败", e);
return response;
抛出的异常如下:
2、BUG定位
415 Unsupported Media Type的意思是指服务端无法处理当前类型的报文,再看代码,我们确实没有在消息头指定消息类型,那么是否把消息头加上就好了,加上后的代码如下:
public Response execute(Request request)
Response response = null;
try
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Request> httpEntity = new HttpEntity<>(request, headers);
response = restTemplate.postForObject(url, httpEntity, Response.class);
catch (Exception e)
log.error("请求失败", e);
return response;
经测试,加上消息头后就可以正常请求了,所以猜测BUG是由于添加jackson-dataformat-xml依赖使得RestTemplate在发起请求时添加了默认消息头导致的,并且这个消息头就是xml的消息头。
为了确认猜测,下面来看下RestTemplate的源码,首先进到postForObject方法中:
public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables) throws RestClientException
RequestCallback requestCallback = httpEntityCallback(request, responseType);
HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor<>(responseType, getMessageConverters(), logger);
return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
可以看到我们传入的消息头被封装成了requestCallback对象,根据调用关系,进入到doExecute方法,其主要代码如下:
try
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null)
requestCallback.doWithRequest(request);
response = request.execute();
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
catch (IOException ex)
// 省略
很明显,RestTemplate在发起请求时,使用requestCallback.doWithRequest(request)这行代码给请求添加消息头,进入方法内,发现RequestCallback是个接口,查看该接口的实现类有两个,都定义在RestTemplate中:
- AcceptHeaderRequestCallback
- HttpEntityRequestCallback
首先看AcceptHeaderRequestCallback.doWithRequest方法,代码如下:
public void doWithRequest(ClientHttpRequest request) throws IOException
if (this.responseType != null)
List<MediaType> allSupportedMediaTypes = getMessageConverters().stream()
.filter(converter -> canReadResponse(this.responseType, converter))
.flatMap(this::getSupportedMediaTypes)
.distinct()
.sorted(MediaType.SPECIFICITY_COMPARATOR)
.collect(Collectors.toList());
if (logger.isDebugEnabled())
logger.debug("Accept=" + allSupportedMediaTypes);
request.getHeaders().setAccept(allSupportedMediaTypes);
看最后一行代码,可以看到这块代码是用于设置消息头中的Accept属性,这个属性是用来告诉服务器,客户端能够处理的消息类型,与我们报的错没啥关系,再看HttpEntityRequestCallback.doWithRequest方法,代码如下:
public void doWithRequest(ClientHttpRequest httpRequest) throws IOException
super.doWithRequest(httpRequest);
Object requestBody = this.requestEntity.getBody();
if (requestBody == null)
// 省略
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())
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
logBody(requestBody, requestContentType, genericConverter);
genericConverter.write(requestBody, requestBodyType, requestContentType, httpRequest);
return;
else if (messageConverter.canWrite(requestBodyClass, requestContentType))
if (!requestHeaders.isEmpty())
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new LinkedList<>(values)));
logBody(requestBody, requestContentType, messageConverter);
((HttpMessageConverter<Object>) messageConverter).write(
requestBody, requestContentType, httpRequest);
return;
String message = "No HttpMessageConverter for " + requestBodyClass.getName();
if (requestContentType != null)
message += " and content type \\"" + requestContentType + "\\"";
throw new RestClientException(message);
可以看到代码中的else部分,当requestBody不为空时,会对messageConverter进行遍历,如果当前messageConverter符合条件,就对消息头进行设置,并且结束方法,那么是否可以做这么一个猜测,新增的jackson-dataformat-xml依赖引入了一个xml相关的messageConverter,并且该messageConverter在遍历过程中顺序排在前面,并且符合条件。
有了猜测,就打个断点,直接debug看看,这里debug的时候要把刚才加上的消息头的代码去掉,我们来复现这个异常场景,如图:
可以看到,debug的结果与我们的猜测一致。
3、BUG修复
从上面debug的图中,可以看到messageConverter的类型为MappingJackson2HttpMessageConverter,先记着,我们来看getMessageConverters方法:
public List<HttpMessageConverter<?>> getMessageConverters()
return this.messageConverters;
可以看到RestTemplate中有个叫messageConverters的属性用来存messageConverter,而这个messageConverter在RestTemplate初始化的时候会被赋值,以MappingJackson2HttpMessageConverter为例:
static
ClassLoader classLoader = RestTemplate.class.getClassLoader();
// 省略
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
// 省略
如果引入了com.fasterxml.jackson.dataformat.xml.XmlMapper类,那么jackson2XmlPresent值就为true,而当该值为true时,就会新增一个MappingJackson2HttpMessageConverter:
public RestTemplate()
// 省略
if (jackson2XmlPresent)
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
// 省略
那么对于这个BUG的解决方案,就有以下三种:
- 手动的给每个RestTemplate调用都加上消息头
- 去除com.fasterxml.jackson.dataformat.xml.XmlMapper类
- 去除MappingJackson2HttpMessageConverter对象
前两种都不太靠谱,我们选用第三种,在RestTemplate注入时,将messageConverters这个list拿出来遍历,去除其中类型为MappingJackson2HttpMessageConverter的对象:
@Bean("restTemplate")
public RestTemplate restTemplate()
RestTemplate restTemplate = new RestTemplate();
HttpMessageConverter<?> xmlConverter = null;
for (HttpMessageConverter<?> messageConverter : restTemplate.getMessageConverters())
if (messageConverter instanceof MappingJackson2XmlHttpMessageConverter)
xmlConverter = messageConverter;
if (xmlConverter != null)
restTemplate.getMessageConverters().remove(xmlConverter);
return restTemplate;
以上是关于记一次RestTemplate消息类型不匹配的BUG定位的主要内容,如果未能解决你的问题,请参考以下文章
记一次解决RestTemplate和HttpClient请求结果乱码的问题