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

  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;

以上是关于记一次RestTemplate消息类型不匹配的BUG定位的主要内容,如果未能解决你的问题,请参考以下文章

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

记一次解决RestTemplate和HttpClient请求结果乱码的问题

记一次EFCore类型转换错误及解决方案

记一次mybatis bindingexception 问题排查

记一次生产kafka消息消费的事故

记一次RocketMQ消息消费异常