記一次SpringBoot返回數據亂碼引發的思考

記一次SpringBoot返回數據亂碼引發的思考

問題背景

使用SpringBoot的starter構建了一個demo項目,添加了一個Get接口,返回一段字符串,有中文、數字和英文,接口一直正常,直到昨天在項目添加了Listener、Filter和Interceptor,接口返回的中文亂碼了。

解決過程

  1. 添加完Listener、Filter和Interceptor(內部都只打印日誌,無業務邏輯)後請求接口返回如下:

  2. 將代碼回滾後,顯示正常:

  3. Debug到AbstractMessageConverterMethodProcessor中,發現StringHttpMessageConverter在write body時用的字符集爲ISO-8859-1,修改defaultCharsetUTF-8後顯示正常;

  4. 搜索HttpMessageConverters的相關資料:

  1. 修改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);
		}
	}
}

遺留問題

  1. 經過多次試驗發現:
    • 回滾Listener、Filter和Interceptor有關的代碼後,Spring內部初始化的HttpMessageConverters中有兩個StringHttpMessageConverter的實例,先初始化的默認編碼爲UTF-8,後初始化的默認編碼爲ISO-8859-1
    • 在進行請求的處理時,因爲先遍歷到初始化的默認編碼爲UTF-8StringHttpMessageConverter,所以寫入響應體的沒有亂碼;
  2. 只要項目中加載了@Configuration public class WebMvcConfigure extends WebMvcConfigurationSupport,初始化的StringHttpMessageConverter的實例只有一個而且默認編碼爲ISO-8859-1,所以在寫入響應體時會出現亂碼。
  3. 未找到上述現象的原因。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章