記一次SpringBoot返回數據亂碼引發的思考
文章目錄
問題背景
使用SpringBoot的starter構建了一個demo項目,添加了一個Get接口,返回一段字符串,有中文、數字和英文,接口一直正常,直到昨天在項目添加了Listener、Filter和Interceptor,接口返回的中文亂碼了。
解決過程
-
添加完Listener、Filter和Interceptor(內部都只打印日誌,無業務邏輯)後請求接口返回如下:
-
將代碼回滾後,顯示正常:
-
Debug到
AbstractMessageConverterMethodProcessor
中,發現StringHttpMessageConverter
在write body時用的字符集爲ISO-8859-1
,修改defaultCharset
爲UTF-8
後顯示正常; -
搜索HttpMessageConverters的相關資料:
- 修改Spring初始化後的StringHttpMessageConverter的默認字符集爲
UTF-8
。
解決方案:
一、指定本請求產生的Response的內容格式
在@RequestMapping
中設置produce:@RequestMapping(value = "/config",produces = {"text/html;charset=UTF-8"})
二、修改Spring的StringHttpMessageConverter
默認字符集
@Configuration
public class WebMvcConfigure extends WebMvcConfigurationSupport {
/* 該方法不建議使用,因爲此方法接管了Spring初始化時對HttpMessageConverters的初始化過程,接管以後,Spring不再對此進行初始化
* 該方法在 extendMessageConverters 之前執行
@Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new StringHttpMessageConverter());
}*/
/**
* 建議使用該方法進行HttpMessageConverters的修改,此時的converters已經是Spring初始化過的
* 因爲加入了WebMvcConfigure,導致Spring的HttpMessageConverters中的String轉換類默認使用ISO-8859-1編碼
* 所以這裏對Spring已經初始化過的StringHttpMessageConverter的編碼進行修改
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.forEach(converter -> {
if (converter instanceof StringHttpMessageConverter){
((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
}
});
}
}
源碼分析
參見:
springMVC源碼分析–HttpMessageConverter數據轉化(一)
springMVC源碼分析–HttpMessageConverter參數read操作(二)
springMVC源碼分析–HttpMessageConverter寫write操作(三)
SpringMVC源碼剖析(五)-消息轉換器HttpMessageConverter
SpringMVC源碼總結(四)由StringHttpMessageConverter引出的客戶端服務器端之間的亂碼過程分析
HttpMessageConverters的初始化過程
暫只做記錄不分析
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setContentNegotiationManager(mvcContentNegotiationManager());
adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
configureAsyncSupport(configurer);
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
return adapter;
}
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
return new RequestMappingHandlerAdapter();
}
}
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
public RequestMappingHandlerAdapter() {
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false); // see SPR-7316
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(stringHttpMessageConverter);
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
}
public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {
public AllEncompassingFormHttpMessageConverter() {
try {
addPartConverter(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
addPartConverter(new JsonbHttpMessageConverter());
}
if (jackson2XmlPresent) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
if (jackson2SmilePresent) {
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
}
}
HttpMessageConverters解析過程
參見上述博客
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
//省略
//根據選擇的媒體類型,遍歷所有的消息轉換器,得到第一個可以對該媒體類型進行正常的轉換器,並使用該轉換器將body寫入響應
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
//省略
}
}
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
}
遺留問題
- 經過多次試驗發現:
- 回滾Listener、Filter和Interceptor有關的代碼後,Spring內部初始化的
HttpMessageConverters
中有兩個StringHttpMessageConverter
的實例,先初始化的默認編碼爲UTF-8
,後初始化的默認編碼爲ISO-8859-1
; - 在進行請求的處理時,因爲先遍歷到初始化的默認編碼爲
UTF-8
的StringHttpMessageConverter
,所以寫入響應體的沒有亂碼;
- 回滾Listener、Filter和Interceptor有關的代碼後,Spring內部初始化的
- 只要項目中加載了
@Configuration public class WebMvcConfigure extends WebMvcConfigurationSupport
,初始化的StringHttpMessageConverter
的實例只有一個而且默認編碼爲ISO-8859-1
,所以在寫入響應體時會出現亂碼。 - 未找到上述現象的原因。