目录

  • 1. 背景
  • 1.1 问题描述
  • 1.2 问题排查


1. 背景

1.1 问题描述

客户反馈,在线上环境,给他们推送的消息中,中文有乱码的,也有没乱码的(如下图)。推送的逻辑是服务A先去服务B查询信息,然后服务A再将查询到的信息推送给客户,乱码就刚好是从服务B查询到的信息。

linux Spring boot 请求乱码 springboot post 中文乱码_默认编码

1.2 问题排查

经过排查,发现在服务A调用服务B查询信息时(RestTemplate 的 postForObject() 方法进行调用),出了问题。服务A没有改动,而服务B有次升级,并发现服务B升完级后就出现了乱码情况。因此就断定了是服务B的问题,继续排查但是发现这次升级代码没有任何更改,只是更改了pom文件中,springboot的依赖版本,版本由2.0.6.RELEASE更改成了2.4.2

升级之前

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.0.6.RELEASE</version>
</parent>

升级之后

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.4.2</version>
</parent>

猜想是不是因为版本更改导致的乱码的产生,就开始撸源码。在撸源码的过程中,经过对比 sprintboot 2.0.6.RELEASE 和 2.4.2 两个版本,发现在类 AbstractJackson2HttpMessageConverter 中的 DEFAULT_CHARSET 字段默认赋值时不一样的。

sprintboot 2.0.6.RELEASE

public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
	public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
}

sprintboot 2.4.2

public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
	/**
	 * The default charset used by the converter.
	 */
	@Nullable
	@Deprecated
	public static final Charset DEFAULT_CHARSET = null;
}

服务A调用服务B查询信息时使用的是 restTemplate.postForObject() 方法。 并且我们对 restTemplate进行了自定义配置。

restTemplate配置如下:

@Configuration
public class RestTemplateConfigurer {

    @Value("${rest.connect-timeout}")
    private Integer connTimeout;

    @Value("${rest.read-timeout}")
    private Integer readTimeout;

    @Primary
    @Bean(name = "restTemplateCommon")
    public RestTemplate restTemplate() {
        return new RestTemplate(requestFactoryCommon());
    }

    @Bean(name = "requestFactoryCommon")
    public HttpComponentsClientHttpRequestFactory requestFactoryCommon() {
        return buildRequestFactory(150, 50, connTimeout, readTimeout);
    }

    private HttpComponentsClientHttpRequestFactory buildRequestFactory(int maxTotal, int maxRoute, int connTimeout, int readTimeout) {
        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
        connectionManager.setMaxTotal(maxTotal);
        connectionManager.setDefaultMaxPerRoute(maxRoute);
        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(readTimeout)
                .setConnectTimeout(connTimeout)
                .setConnectionRequestTimeout(1000)
                .build();
        return new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(connectionManager)
                .build());
    }
}

发送POST请求封装:

@Component
public class RestUtils {

    @Resource(name = "restTemplateCommon")
    private RestTemplate restTemplate;

    /**
     * 发送POST请求
     */
    public String postJsonParam(String url, JSONObject param) {
        String resp;
        HttpHeaders headers = new HttpHeaders();
		// 添加下面代码即可解决乱码问题
		// headers.add(HttpHeaders.ACCEPT, "application/json;charset=utf-8");
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity httpEntity = new HttpEntity(param, headers);
        resp = restTemplate.postForObject(url, httpEntity, String.class);
        return resp;
    }
}

产生乱码的原因概述:
在升级版本之前,服务A发起post调用,然后服务B根据服务A的请求参数,从数据库获取数据。服务B将获取到的数据封装到Response中,首先将数据进行编码(使用UTF-8),然后设置Response的header的Content-type属性的值 application/json,设置时需要查看默认编码,经查找使用默认编码为UTF-8,因此Content-type最终的值为 application/json;charset=utf-8。然后返回给服务A,服务A通过UTF-8对Response的body进行解码,因此就不出现乱码(因为服务B是使用的是UTF-8编码的)。

在升级本版之后,唯一与升级之前的不同就是在Response的header的Content-type属性时出现了差异,虽然设置的Content-type的格式还是 application/json 但是要设置编码时发现,默认编码为空,因此最终因此Content-type最终的值为 application/json。然后返回给服务A,服务A收到数据后对Response的body进行解码,发现Content-type没有设置编码格式,因此就是用默认的编码进行解码(RestTemplate 默认编码为ISO-8859-1),编码使用UTF-8,解码使用ISO-8859-1,因此就出现了乱码。

解决办法:

  • 客户端处理
  • 在发送请求时,添加 headers.add(HttpHeaders.ACCEPT, “application/json;charset=utf-8”);
  • 服务端处理
#方式一
server:
  servlet:
    encoding:
      charset: utf-8
      #只设置Response
      #HttpEncodingAutoConfiguration#CharacterEncodingFilter中会使用forceResponse
      #CharacterEncodingFilter#doFilterInternal中会设置HttpServletResponse.setCharacterEncoding
      forceResponse: true
      enabled: true
 
#方式二
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
  @Override
  protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    for (HttpMessageConverter<?> converter : converters) {
      if (converter instanceof MappingJackson2HttpMessageConverter) {
        ((MappingJackson2HttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
     }
    }
  }