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


    1、前因

    由于跟第三方交互是用的XML协议,并且之前的代码用的很老的XML解析方法,解析效率不高,所以这次做需求的时候,打算使用Jackson来解析XML报文,因此在项目中加入了以下依赖:

    <dependency>
        <groupId>com.fasterxml.jackson.dataformatgroupId>
        <artifactId>jackson-dataformat-xmlartifactId>
        <version>2.13.3version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结果导致了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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    抛出的异常如下:
    异常

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    经测试,加上消息头后就可以正常请求了,所以猜测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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可以看到我们传入的消息头被封装成了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) {
    	// 省略
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    很明显,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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    看最后一行代码,可以看到这块代码是用于设置消息头中的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);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    可以看到代码中的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;
    }
    
    • 1
    • 2
    • 3

    可以看到RestTemplate中有个叫messageConverters的属性用来存messageConverter,而这个messageConverter在RestTemplate初始化的时候会被赋值,以MappingJackson2HttpMessageConverter为例:

    static {
    	ClassLoader classLoader = RestTemplate.class.getClassLoader();
    	// 省略
    	jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
    	// 省略
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    如果引入了com.fasterxml.jackson.dataformat.xml.XmlMapper类,那么jackson2XmlPresent值就为true,而当该值为true时,就会新增一个MappingJackson2HttpMessageConverter:

    public RestTemplate() {
    	// 省略
    	if (jackson2XmlPresent) {
    		this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
    	}
    	// 省略
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么对于这个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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
  • 相关阅读:
    Python Flask框架学习教程
    10_分类和static
    【Java篇】有关字节、字符流的知识你还记得吗?
    什么是PLC?
    【光学】基于Matlab模拟干涉条纹图
    列的完整性约束——调整列的完整性约束
    5G商企专网,助力打造城市生命线“安徽样板”
    双指针算法解决 移动零 和 复写零问题
    Java复习第二弹!
    SpringBoot+MyBatis flex实现简单增删改查
  • 原文地址:https://blog.csdn.net/Stone__Fly/article/details/126452905