目录
- 1. 背景
- 1.1 问题描述
- 1.2 问题排查
1. 背景
1.1 问题描述
客户反馈,在线上环境,给他们推送的消息中,中文有乱码的,也有没乱码的(如下图)。推送的逻辑是服务A先去服务B查询信息,然后服务A再将查询到的信息推送给客户,乱码就刚好是从服务B查询到的信息。
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);
}
}
}