由于跟第三方交互是用的XML协议,并且之前的代码用的很老的XML解析方法,解析效率不高,所以这次做需求的时候,打算使用Jackson来解析XML报文,因此在项目中加入了以下依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformatgroupId>
<artifactId>jackson-dataformat-xmlartifactId>
<version>2.13.3version>
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;
}
抛出的异常如下:
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.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的结果与我们的猜测一致。
从上面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注入时,将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;
}