分析基于springboot 2.2.1,其他版本源码可能略有区别
编码问题
在编程中我们经常遇到中文乱码问题,主要分为以下几种:
- 返回一个页面
- 返回一个string类型且方法注释了@ResponseBody注解
- 返回一个json数据且方法注释了@ResponseBody注解
下面依次看看每种情况
1.1 返回页面乱码
这种情况在springboot中已经看不到了,因为springboot已经帮我们做了编码的自动配置为utf-8
首先看看正常页面编码是如何设置的:
在HttpEncodingAutoConfiguration
这个自动配置类中,注入了CharacterEncodingFilter
。
private final HttpProperties.Encoding properties;
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter;
}
设置这个filter的编码为配置文件中配置的字符集,默认情况下为UTF-8,我们也可以显式的指定
# 编码设置
spring:
http:
encoding:
force-request: true # 是否强制request都使用该种编码
force-response: true # 是否强制response都使用该种编码
charset: UTF-8
enabled: true
而在这个filter中则做了如下的设置:
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String encoding = getEncoding();
if (encoding != null) {
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
即设置了response的字符集。因此当返回正常页面的时候不需要我们去设置编码方式了
接下来看看错误页面的编码情况
前面分析过,默认的错误视图为StaticView
,下面看看其中的render方法
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
//...
}
可以看到,这里会设置编码为UTF-8
1.2 返回一个string类型
1.2.1 原因
这种情况下会出现乱码:
@RequestMapping("/test9")
@ResponseBody
public String test9(){
return "成功";
}
由于返回的是注释了@ResponseBody注解的,因此会经过RequestResponseBodyMethodProcessor
这个处理器处理返回值
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)) {}
前面分析过,在处理返回值的时候,会依次从容器中的messageConverters
选取一个能够使用的。而默认情况下的组件如下图所示
我们需要关注StringHttpMessageConverter
和MapppingJackson2HttpMessageConverter
由于前者优先触发,因此当我们返回一个字符串时,会使用这个converter,在这个converter中,字符集被定义为
public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
因此,这里会出现乱码
1.2.2 解决方案
可以给RequestMappingHandlerAdapter重新设置messageConverter
在实现了WebMvcConfigurer
接口的配置类中重写如下的方法:即可解决string类型返回值乱码
@Autowired
private StringHttpMessageConverter stringHttpMessageConverter;
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(stringHttpMessageConverter);
}
注意事项:
- 这里采用的springboot自动注入的
StringHttpMessageConverter
,这个采用的是utf-8编码 - 这样设置后会覆盖容器中默认的converter,导致容器中只剩下这个,因此需要手动设置其他,但是这样做太麻烦,需要复制
WebMvcConfigurationSupport
中的addDefaultHttpMessageConverters
。
下面给出第二种方案:
我们在前面自定义RedisSessionAttributeStore中(见springmvc之HandlerAdapter
),重新注入了RequestMappingHandlerAdapter
,因此这里我们可以用同样的方式,配置converters
@Autowired
private StringHttpMessageConverter stringHttpMessageConverter;
/**
* 使用RedisSessionAttributeStore代替默认的session存储
* @return
*/
@Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
RedisSessionAttributeStore sessionAttributeStore = new RedisSessionAttributeStore(redisCacheClient);
adapter.setSessionAttributeStore(sessionAttributeStore);
//首先获取父类设置好的converters
List<HttpMessageConverter<?>> messageConverters = getMessageConverters();
//然后将其中的StringHttpMessageConverter进行替换
for (int i = 0; i < messageConverters.size(); i++) {
if (messageConverters.get(i) instanceof StringHttpMessageConverter) {
messageConverters.set(i, stringHttpMessageConverter);
}
}
return adapter;
}
通过这种思路,下面有第三种方案
同样是在MvcConfig中,还有下面这样的方法extendMessageConverters
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
for (int i = 0; i < messageConverters.size(); i++) {
if (messageConverters.get(i) instanceof StringHttpMessageConverter) {
messageConverters.set(i, stringHttpMessageConverter);
}
}
}
这个方法与configureMessageConverters
的区别就是,前者只是拓展,延伸converters,而后者是完全覆盖,相关逻辑可以见WebMvcConfigurationSupport
中的getMessageConverters
。
因此和第二种方案一样,我们重新设置StringHttoMessageConverter
1.3 返回一个对象
@RequestMapping("/test18")
@ResponseBody
public ResponseResult test18() {
return ResponseResult.ok("成功");
}
这种情况在springboot中也不会乱码,原因如下:
上面分析可知,这里用到了MapppingJackson2HttpMessageConverter
这个类在处理编码的时候见如下语句:
AbstractJackson2HttpMessageConverter
的writeInternal
方法
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) {
if (contentType != null && contentType.getCharset() != null) {
Charset charset = contentType.getCharset();
for (JsonEncoding encoding : JsonEncoding.values()) {
if (charset.name().equals(encoding.getJavaName())) {
return encoding;
}
}
}
return JsonEncoding.UTF8;
}
可以看到,如果没有特别指定,则使用utf-8编码