10-补充

2022-07-18

10-补充

springmvc之补充

一. 编码问题

在编程中我们经常遇到中文乱码问题,主要分为以下几种:

  1. 返回一个页面
  2. 返回一个string类型且方法注释了@ResponseBody注解
  3. 返回一个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选取一个能够使用的。而默认情况下的组件如下图所示

image-20210622153118189

我们需要关注StringHttpMessageConverterMapppingJackson2HttpMessageConverter

由于前者优先触发,因此当我们返回一个字符串时,会使用这个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);
    }

注意事项:

  1. 这里采用的springboot自动注入的StringHttpMessageConverter,这个采用的是utf-8编码
  2. 这样设置后会覆盖容器中默认的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

这个类在处理编码的时候见如下语句:

AbstractJackson2HttpMessageConverterwriteInternal方法

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编码