文章目錄
兩個問題
- spring boot
RestTemplate
在運行一段時間後居然報空指針異常,可以根據StackTrace定位到是有一個HttpMessageConverter
爲空; - 一個接口返回的
Content-type
爲text/pain
,但返回結果總是被雙引號包圍,導致接口調用方解析失敗,可以定位到是由於一個自定義的HttpMessageConverter
錯誤的攔截了text/plain
的produce
類型,並在字符串兩端加上了雙引號。
HttpMessageConverter作用
HttpMessageConverter
可以把不同類型的body轉爲Java對象,也可以吧Java對象轉爲滿足要求的body,在序列化與反序列化中有非常重要的作用。
HttpMessageConverter匹配規則
Http
請求頭中會包含Accept
,告知服務器要傳回什麼樣的數據,如Accept: application/json, text/javascript, */*; q=0.01
;同時會指定Content-Type
告知服務器本次body傳輸的參數數據是什麼類型,服務器可以根據數據類型轉化爲服務器內部對象並提取參數;Server
端接收到請求,會判斷自己是否支持客戶端傳過來的參數類型(MediaType
),如果沒有任何一個支持,服務器將返回406(HttpMediaTypeNotSupportedException
);Server
處理完參數和邏輯,準備根據客戶端要求的Accept
返回,這時服務端又會判斷自己是否支持返回這種類型(MediaType
),如果不支持,服務器將返回406(HttpMediaTypeNotAcceptableException
);HttpMessageConverter
的作用就是服務器判斷自己是否支持某種MediaType
;HttpMessageConverter
不是僅僅只有一個而是一個列表,通過責任鏈的方式匹配:循序遍歷所有HttpMessageConverter
,調用其canRead()
方法,若返回true表示可以處理,一旦有某個HttpMessageConverter
可以處理某一請求的參數MediaType
,就是用這個HttpMessageConverter
的read()
方法讀取參數,一旦處理完數據即將返回,又用同樣的方法遍歷HttpMessageConverter
列表,找出第一個canWrite()
返回true的HttpMessageConverter
,調用其write()
方法返回給客戶端。
HttpMessageConverter初始化時序圖
- 在
ApplicationContext
refresh期間,HttpMessageConverter
開始初始化,初始化分別在三個類中進行:其中RequestMappingHandlerAdapter
和HttpMessageConvertersAutoConfiguration
是同時行的互不干擾,HttpMessageConverters
的初始化需要在前兩個類初始化完成後才能進行; RequestMappingHandlerAdapter
初始化默認HttpMessageConverter
列表- 調用所有
WebMvcConfigurer
類型的自定義配置類(@Configuration
)的configureMessageConverters
方法初始化HttpMessageConverter
列表; - 如果沒有自定義的
WebMvcConfigurer
配置,調用addDefaultHttpMessageConverters
方法初始化HttpMessageConverter
列表,默認HttpMessageConverter
列表都是根據ClassLoader
中是否加載否一個特定類來判斷某一個HttpMessageConverter
是否需要加到默認列表中,並且在最後做了一下排序,僅僅是把xml類型的轉換器放到了目前的HttpMessageConverter
列表最後; - 調用所有
WebMvcConfigurer
類型的自定義配置類的extendMessageConverters
方法擴展HttpMessageConverter
列表,直接加在列表尾部
- 調用所有
HttpMessageConvertersAutoConfiguration
類將所有HttpMessageConverter
類型的組件(@Conponent/@Bean
等),初始化到一個有上下文提供的HttpMessageConverter
列表中;HttpMessageConverters
初始化時,將2和3兩個列表合併,如果上下文提供的和默認列表中有重複但對象並非是同一個,會把上下文提供的HttpMessageConverter
和默認列表中的HttpMessageConverter
放在相鄰的位置,並且會把上下文提供的放在前面;把所有上下文提供的且不在默認列表中的HttpMessageConverter
放在整個合併列表的最前面,上下文提供的HttpMessageConverter
順序由類上的@Order(value=1)
註解指定,value值越小越靠前,優先級越高。
自定義HttpMessageConverter
package com.enmo.dbaas.common.config.feigninterceptor;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import javax.activation.UnsupportedDataTypeException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
/**
* Created by IntelliJ IDEA
*
* @author chenlei
* @date 2020/1/11
* @time 17:44
* @desc AHttpMessageConverter
*/
@Component
@Order(1)
public class AHttpMessageConverter extends AbstractHttpMessageConverter<Object> implements GenericHttpMessageConverter<Object> {
@Override
protected boolean supports(Class<?> clazz) {
return true;
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
outputMessage.getBody().write("{}".getBytes());
outputMessage.getBody().flush();
}
@Override
public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
return true;
}
@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
InputStream in = inputMessage.getBody();
if (String.class.getTypeName().equals(type.getTypeName())) {
byte[] bytes = new byte[65536];
int offset = 0;
while(true) {
int readCount = in.read(bytes, offset, bytes.length - offset);
if(readCount == -1) {
return new String(bytes, "UTF-8");
}
offset += readCount;
if(offset == bytes.length) {
byte[] newBytes = new byte[bytes.length * 3 / 2];
System.arraycopy(bytes, 0, newBytes, 0, bytes.length);
bytes = newBytes;
}
}
}
throw new UnsupportedDataTypeException(type.getClass().getTypeName());
}
@Override
public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
return true;
}
@Override
public void write(Object o, @Nullable Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
super.write(o, contentType, outputMessage);
}
}
自定義一個AHttpMessageConverter
,canRead()
和canWrite()
犯法都直接返回true
,並且把@Order
值設置爲1
,因爲使用Component
註解的上下文HttpMessageConverter
列表回被添加在HttpMessageConverter
前面,而spring
默認實現的HttpMessageConverter
order值都是Integer.MAX_VALUE
,因此AHttpMessageConverter
會匹配所有Content-Type
和Accept
。
類中read()
方法只支持String
類型的參數,其他類型的參數一律拋出UnsupportedDataTypeException
;
類中write()
方法均返回字符串"{}"
。
假設在Controller
中寫一個這樣的方法:
@PostMapping("install/invoke/test")
public ResultData invokeTest(@RequestHeader String a,
@RequestParam String b,
@RequestParam String c,
@RequestBody JSONObject d) {
log.info("{}, {}, {} ,{}", a, b, c, d);
return new JSONObject().fluentPut("key", "value");
}
將報錯:
org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is javax.activation.UnsupportedDataTypeException: java.lang.Class
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:216)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:157)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:130)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:660)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter.doFilterInternal(HttpTraceFilter.java:88)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.filterAndRecordMetrics(WebMvcMetricsFilter.java:114)
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:104)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:109)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1587)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)
Caused by: javax.activation.UnsupportedDataTypeException: java.lang.Class
at com.enmo.dbaas.common.config.feigninterceptor.AHttpMessageConverter.read(AHttpMessageConverter.java:72)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:204)
... 62 common frames omitted
如果修改JSONObject
爲String
:
@PostMapping("install/invoke/test")
public ResultData invokeTest(@RequestHeader String a,
@RequestParam String b,
@RequestParam String c,
@RequestBody String d) {
log.info("{}, {}, {} ,{}", a, b, c, d);
return new JSONObject().fluentPut("key", "value");
}
將返回:
{}
而不是:
{
"key": "value"
}
說明正是Accept
和Content-Type
都使用了AHttpMessageConverter
來序列化和反序列化。
解決問題
RestTemplate NPE
spring boot RestTemplate
在運行一段時間後居然報空指針異常,可以根據StackTrace定位到是有一個HttpMessageConverter
爲空
過一段時間才爲空,肯定是運行過程中有地方把HttpMessageConverter
列表某個索引的值改爲了null
,不是一啓動就報錯,也不是調用特定接口報錯,而是某一次調用出現NPE後,之後的任意一次請求全部報NPE。
恰好找到了這篇文章:處理restTemplate的messageConverters設置StringHttpMessageConverter
文中提到每次調用RestTemplate
的時候手動設置字符集爲UTF-8
,線上偶發NPE,這與我們的設置極爲相似:
restTemplate.getMessageConverters()
.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
我們知道HttpMessageConverters
類中的List<HttpMessageConverter>
只是一個簡單的UnmodifiableList<HttpMessageConverter<?>>
不支持高併發,這樣要麼會導致List<HttpMessageConverter>
無限膨脹,要麼會導致空指針,因爲List.add(int index, Object o)
原理是先把列表index之後的所有元素後移一位,然後再把索引爲index的位置賦值。spring的組件默認是單例的,在高併發的情況下,所有線程同時修改一個不支持高併發的列表某,一個線程剛好把所有元素後移一位u,第一個元素還沒來得及賦值,可能恰好另外一個線程已經開始遍歷,取到第一個HttpMessageConverter
恰好是null
,於是開始報空指針,這是偶發原因;
那爲什麼可能出現一次後,就有可能之後所有的請求全部報空指針呢,特別是在凌晨有多個定時任務同時跑的情況下,出現“永久NPE”的概率極大。這是因爲剛好有兩個線程同時設置索引爲0的HttpMessageConverter
,其中一個線程剛剛把所有元素後移一位,還沒來得及給索引爲0的位置賦值,另一個線程又開始把所有元素後移,導致索引爲1的元素永久爲空,再也救不回來了。
找的文章裏的解決方案和單例模式類似,但也並不能完全避免高併發問題,還是需要加同步塊並且使用duoble check,這種方式非常不優雅,因爲spring 的bean默認是單例,我們完全可以在初始化的時候配置好,後面直接注入使用即可,不需要二次配置:
@Configuration
public class HttpConfiguration {
@Bean
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
messageConverters.removeIf(converter -> converter instanceof StringHttpMessageConverter);
messageConverters.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}
}
text/pain有引號
一個接口返回的Content-type
爲text/pain
,但返回結果總是被雙引號包圍,導致接口調用方解析失敗,可以定位到是由於一個自定義的HttpMessageConverter
錯誤的攔截了text/plain
的produce
類型,並在字符串兩端加上了雙引號。
理解上述原理後,應該可以猜想到應該是由錯誤的HttpMessageConverter
序列化導致的,在不配置任何自定義HttpMessageConverter
的時候一切正常,我們只加了一個自定義的FastJsonHttpMessageConverter
:
package com.enmo.dbaas.common.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class FastJsonHttpMessageConverterEx extends FastJsonHttpMessageConverter {
public FastJsonHttpMessageConverterEx() {
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); // 自定義時間格式
fastJsonConfig.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteMapNullValue); // 正常轉換 null 值
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML);
supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM);
supportedMediaTypes.add(MediaType.APPLICATION_PDF);
supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML);
supportedMediaTypes.add(MediaType.APPLICATION_XML);
supportedMediaTypes.add(MediaType.IMAGE_GIF);
supportedMediaTypes.add(MediaType.IMAGE_JPEG);
supportedMediaTypes.add(MediaType.IMAGE_PNG);
supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM);
supportedMediaTypes.add(MediaType.TEXT_HTML);
supportedMediaTypes.add(MediaType.TEXT_MARKDOWN);
supportedMediaTypes.add(MediaType.TEXT_XML);
supportedMediaTypes.add(MediaType.TEXT_PLAIN);
this.setSupportedMediaTypes(supportedMediaTypes);
this.setFastJsonConfig(fastJsonConfig);
}
}
這是上下文提供的,會放在HttpMessageConverter
列表的第一個,優先級最高。
結合Fastjson
序列化字符串的時候會加上引號:
因此肯定是這個FastJsonHttpMessageConverterEx
的鍋,果不其然,FastJsonHttpMessageConverter
的canRead()
方法和canWrite()
方法都是是根據List<MediaType> supportedMediaTypes
來判斷的:只要是List<MediaType> supportedMediaTypes
定義的MediaType
,都由FastJsonHttpMessageConverterEx
來序列化和反序列化,而我們自定的FastJsonHttpMessageConverterEx
加上了supportedMediaTypes.add(MediaType.TEXT_PLAIN);
,不出錯就奇怪了。
刪掉該行,問題解決,並且FastJsonHttpMessageConverterEx
該不該處理這麼多類型還需要多考量,雖然目前系統運作良好,但是不保證新業務增加後會出現問題,因爲目前我們只有applicaition/json;plain/text;application/object-stream
等幾種類型,假設哪天出現一個image/jpeg
,保不齊又會出錯,因爲可能Fastjson
處理不了但又強行讓其處理,也可能強行處理了,結果格式不對等。