记一次RestTemplate消息类型不匹配的BUG定位

1、前因

由于跟第三方交互是用的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;
}

抛出的异常如下:
异常

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的解决方案,就有以下三种:

  1. 手动的给每个RestTemplate调用都加上消息头
  2. 去除com.fasterxml.jackson.dataformat.xml.XmlMapper类
  3. 去除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;
}

你可能感兴趣的:(BUG大杂烩,java,xml,RestTemplate,jackson,header)