本人运用RestTemplate联调一个上游接口,本地请求成功,放到测试环境就请求失败,报错是上游提示我这边没有传参。相同的代码,在不同的环境会报错,第一时间就是请求RestTemplate版本不一致。后来也去debug,结果如下:
//异常请求
14:32:04.685 [main] DEBUG org.springframework.web.client.RestTemplate - Writing [{proj_ids=["39***f"]}] using [org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter@34f5090e]
{"body":"{\"retCode\":-1,\"errMsg\":\"缺少必要参数: proj_ids\"}}
//正常请求
14:34:58.477 [main] DEBUG org.springframework.web.client.RestTemplate - Writing [{proj_ids=["39****6f"]}] with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
{"body":"{\"retCode\":0,\"data\":{\"items\":[{\"id\":8,\"name\":\"1\"},{\"id\":9,\"name\":\"kkty团队\"}]}}"}
大家不难看出,其实debug结果已经给出答案。异常的报错是因为 RestTemplate Write的时候使用的是MappingJackson2XmlHttpMessageConverter,而正常MappingJackson2HttpMessageConverter,其实对应的是Content-Type问题,即:
Content-Type | |
MappingJackson2XmlHttpMessageConverter | application/xml |
MappingJackson2HttpMessageConverter | application/json |
到这里,我说明下,我联调正常请求的RestTemplate所JAR版本是 spring-web-5.2.10.RELEASE,而异常请求的是 spring-web-4.3.25.RELEASE。
下面就进入到源码看原因:
进过一系列debug,跟进到了HttpEntityRequestCallback.doWithRequest()方法
@Override
@SuppressWarnings("unchecked")
public void doWithRequest(ClientHttpRequest httpRequest) throws IOException {
super.doWithRequest(httpRequest);
Object requestBody = this.requestEntity.getBody();
if (requestBody == null) {//这里是判断如果请求体是空的
HttpHeaders httpHeaders = httpRequest.getHeaders();
HttpHeaders requestHeaders = this.requestEntity.getHeaders();
if (!requestHeaders.isEmpty()) {
requestHeaders.forEach((key, values) -> httpHeaders.put(key, new ArrayList<>(values)));
}
if (httpHeaders.getContentLength() < 0) {
httpHeaders.setContentLength(0L);
}
}
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();//这里是获取我们是否指定了Content-Type
/*重点是这里,getMessageConverters()方法是获取RestTemplate的转换器列表。
然后debug发现,spring-web-4.3.25.RELEASE版本首个遍历出来的是MappingJackson2XmlHttpMessageConverter,然后符合GenericHttpMessageConverter实例的对象。然后就走下面得逻辑去请求上游。而spring-web-5.2.10.RELEASE首个遍历出来的是MappingJackson2HttpMessageConverter。所以大家都懂了吧,接下来看下转换器如何添加进来这些转换器的。
*/
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 ArrayList<>(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 ArrayList<>(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);
}
}
private void logBody(Object body, @Nullable MediaType mediaType, HttpMessageConverter<?> converter) {
if (logger.isDebugEnabled()) {
if (mediaType != null) {
logger.debug("Writing [" + body + "] as \"" + mediaType + "\"");
}
else {
//这里是输出我们的参数是是使用了那种转换器
logger.debug("Writing [" + body + "] with " + converter.getClass().getName());
}
}
}
看到上面代码后,我们看下转换器如何添加的问题。我这边直接截取spring-web-5.2.10.RELEASE的RestTemplate源码分析。
static {
ClassLoader classLoader = RestTemplate.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
//spring-web-5.2.10.RELEASE以及spring-web-4.3.25.RELEASE jackson2Present 的值都是true
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
/*spring-web-4.3.25.RELEASE jackson2XmlPresent 的值是false,spring-web-5.2.10.RELEASE jackson2XmlPresent 的值是true*/
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
}
接下如截图所示,初始化 RestTemplate 的时候消息转换器messageConverters就会根据true/false将相关Content-Type转换器添加进去,遍历的时候就会将第一个加进去的Content-Type转换器遍历出来作为请求的Content-Type转换器类型。
结合上来来看,说白了就是 不同的spring-web版本导致RestTemplate初始化的时候,将决定优先将哪个Content-Type转换器添加至消息转换器messageConverters。优先添加的那个Content-Type转换器就是本次请求的Content-Type转换器类型。
解决办法:
就很简单了,直接RestTemplate执行请求的时候,指定Content-Type即可,而不是让系统默认。
其实debug的时候就知道问题了,但是还是多去看看源码,多了解下发生问题根本原因。今后开发或排查问题就会更有思路。