场景:某次上线后,导致别的接口RestTemplate调用出现了异常。
1. 起因
经过排查后发现,某次上线的需求增加了该依赖:
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
2.11.0
compile
被影响的接口:
@Slf4j
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
//测试http调用
@RequestMapping(value = "/test3")
public OrderDto get3() {
String url = "http://localhost:8011/consumer/dept/get";
OrderDto orderDto = new OrderDto();
orderDto.setId(100110L);
orderDto.setName("tom is mao");
ResponseEntity results = restTemplate.postForEntity(url, orderDto, String.class);
log.info("打印响应报文{}...", results);
return orderDto;
}
}
全局RestTemplate配置:
@Configuration
@Slf4j
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public HttpComponentsClientHttpRequestFactory clientHttpRequestFactory() {
try {
HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
RegistryBuilder socketFactoryRegistry = RegistryBuilder.create().
register("http", PlainConnectionSocketFactory.getSocketFactory());// 注册http和https请求
// 开始设置连接池
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry.build());
poolingHttpClientConnectionManager.setMaxTotal(500); // 最大连接数500
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); // 同路由并发数100
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)); // 重试次数
HttpClient httpClient = httpClientBuilder.build();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); // httpClient连接配置
clientHttpRequestFactory.setConnectTimeout(20000); // 连接超时
clientHttpRequestFactory.setReadTimeout(30000); // 数据读取超时时间
clientHttpRequestFactory.setConnectionRequestTimeout(20000); // 连接不够用的等待时间
return clientHttpRequestFactory;
} catch (Exception e) {
log.error("初始化HTTP连接池出错", e);
}
return null;
}
}
经过排查后发现:发送的请求报文被序列化成了xml格式,但该接口之前为JSON格式。
2. 解决方案
2.1 方案一
- 显式的声明
Content-Type
的类型:
@Slf4j
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
//测试http调用
@RequestMapping(value = "/test5")
public OrderDto get5() {
String url = "http://localhost:8011/consumer/dept/get";
OrderDto orderDto = new OrderDto();
orderDto.setId(100110L);
orderDto.setName("tom is mao");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity httpEntity = new HttpEntity<>(orderDto,headers);
ResponseEntity results = restTemplate.postForEntity(url, httpEntity, String.class);
log.info("打印响应报文{}...", results);
return orderDto;
}
}
2.2 方案二
本次被影响的接口由于没有显式的声明请求的Content-type类型(即使用RestTemplate默认的消息转换器)。而由于引入了jackson-dataformat-xml
依赖,导致消息转换器由JSON格式转换为XML格式。
2.2.1 消息转换器参与序列化
源码分析:org.springframework.web.client.RestTemplate.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 LinkedList<>(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();
//若requestBody不为空,遍历消息转换器,获取到适合的消息转换
for (HttpMessageConverter> messageConverter : getMessageConverters()) {
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter
由此可以说明,优先级高的消息转换器将会生效。
2.2.2 消息转换器被初始化
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
static {
ClassLoader classLoader = RestTemplate.class.getClassLoader();
romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader);
jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", classLoader);
jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
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);
}
public RestTemplate() {
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter(false));
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
this.messageConverters.add(new AtomFeedHttpMessageConverter());
this.messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
else if (jaxb2Present) {
this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
this.messageConverters.add(new JsonbHttpMessageConverter());
}
if (jackson2SmilePresent) {
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
}
if (jackson2CborPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}
this.uriTemplateHandler = initUriTemplateHandler();
}
}
在调用RestTemplate的构造方法时,将消息转换器放入到集合中。而决定是否加入messageConverters
在静态代码块(是否引入了某些依赖)。
由于某次需求引入了jackson-dataformat-xml
依赖,导致jackson2XmlPresent
返回true,由此:
xml优先级比json高。
解决方案:全局修改转换器优先级。
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
//获取消息转换器
List> messageConverters = restTemplate.getMessageConverters();
messageConverters.add(5,new MappingJackson2HttpMessageConverter());
return restTemplate;
}
3. 源码注意事项
当选中messageConverter
后,其org.springframework.http.converter.GenericHttpMessageConverter#canWrite
方法决定是否使用该消息处理器。
以Jackson为例:org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#canWrite
@Override
public boolean canWrite(Class> clazz, @Nullable MediaType mediaType) {
//判断content-type是否合适,不合适直接返回false
if (!canWrite(mediaType)) {
return false;
}
//判断是否可以使用objectMapper进行序列化,若可以才会返回true、
AtomicReference causeRef = new AtomicReference<>();
if (this.objectMapper.canSerialize(clazz, causeRef)) {
return true;
}
logWarningIfNecessary(clazz, causeRef.get());
return false;
}
protected boolean canWrite(@Nullable MediaType mediaType) {
//若mediaType为空,或者all那么支持
if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
return true;
}
//判断子类是否支持该content-type
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
return true;
}
}
return false;
}
因为我们没有显式的声明content-type,那么MappingJackson2XmlHttpMessageConverter
也可以支持序列化的方式。