場景:某次上線后,導致別的接口RestTemplate調(diào)用出現(xiàn)了異常迫皱。
1. 起因
經(jīng)過排查后發(fā)現(xiàn),某次上線的需求增加了該依賴:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.11.0</version>
<scope>compile</scope>
</dependency>
被影響的接口:
@Slf4j
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
//測試http調(diào)用
@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<String> results = restTemplate.postForEntity(url, orderDto, String.class);
log.info("打印響應(yīng)報文{}...", 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<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().
register("http", PlainConnectionSocketFactory.getSocketFactory());// 注冊http和https請求
// 開始設(shè)置連接池
PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry.build());
poolingHttpClientConnectionManager.setMaxTotal(500); // 最大連接數(shù)500
poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); // 同路由并發(fā)數(shù)100
httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)); // 重試次數(shù)
HttpClient httpClient = httpClientBuilder.build();
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); // httpClient連接配置
clientHttpRequestFactory.setConnectTimeout(20000); // 連接超時
clientHttpRequestFactory.setReadTimeout(30000); // 數(shù)據(jù)讀取超時時間
clientHttpRequestFactory.setConnectionRequestTimeout(20000); // 連接不夠用的等待時間
return clientHttpRequestFactory;
} catch (Exception e) {
log.error("初始化HTTP連接池出錯", e);
}
return null;
}
}
經(jīng)過排查后發(fā)現(xiàn):發(fā)送的請求報文被序列化成了xml格式箩做,但該接口之前為JSON格式近忙。
2. 解決方案
2.1 方案一
- 顯式的聲明
Content-Type
的類型:
@Slf4j
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
//測試http調(diào)用
@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<OrderDto> httpEntity = new HttpEntity<>(orderDto,headers);
ResponseEntity<String> results = restTemplate.postForEntity(url, httpEntity, String.class);
log.info("打印響應(yīng)報文{}...", results);
return orderDto;
}
}
2.2 方案二
本次被影響的接口由于沒有顯式的聲明請求的Content-type類型(即使用RestTemplate默認的消息轉(zhuǎn)換器)。而由于引入了jackson-dataformat-xml
依賴嘱兼,導致消息轉(zhuǎn)換器由JSON格式轉(zhuǎn)換為XML格式国葬。
2.2.1 消息轉(zhuǎn)換器參與序列化
源碼分析: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不為空,遍歷消息轉(zhuǎn)換器芹壕,獲取到適合的消息轉(zhuǎn)換
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<Object> genericConverter =
(GenericHttpMessageConverter<Object>) messageConverter;
//判斷是否可以使用該消息轉(zhuǎn)換器去序列化汇四。
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);
//填充完Request對象后,結(jié)束方法踢涌。
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);
}
}
由此可以說明通孽,優(yōu)先級高的消息轉(zhuǎn)換器將會生效。
2.2.2 消息轉(zhuǎn)換器被初始化
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();
}
}
在調(diào)用RestTemplate的構(gòu)造方法時睁壁,將消息轉(zhuǎn)換器放入到集合中背苦。而決定是否加入messageConverters
在靜態(tài)代碼塊(是否引入了某些依賴)。
由于某次需求引入了jackson-dataformat-xml
依賴潘明,導致jackson2XmlPresent
返回true行剂,由此:
xml優(yōu)先級比json高。
解決方案:全局修改轉(zhuǎn)換器優(yōu)先級钳降。
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
//獲取消息轉(zhuǎn)換器
List<HttpMessageConverter<?>> 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<Throwable> 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
也可以支持序列化的方式。