背景
服务A通过RestTemplate调用服务B,服务B升级后,出现了中文乱码。
框架版本
服务A spring-web 版本
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.0.7.RELEASE</version>
<scope>compile</scope>
</dependency>
服务B spring-web版本
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<!-- 升级前<version>4.3.19.RELEASE</version>-->
<version>5.0.7.RELEASE</version>
<scope>compile</scope>
</dependency>
服务A的调用代码
String result = restTemplate.postForObject(url, formEntity, String.class);
服务B代码
@PostMapping(value = "/getSmsTemplateList")
@ResponseBody
public PageDTO<SmsTemplateDTO> getSmsTemplateList(@RequestBody PageQueryDTO pageQueryDTO) {
return smsTemplateService.querySmsTemplateList(pageQueryDTO);
}
Debug 看A服务的Response 解码
调用堆栈
-
org.springframework.web.client.RestTemplate#postForObject(java.lang.String, java.lang.Object, java.lang.Class<T>, java.lang.Object...)
-
org.springframework.web.client.RestTemplate#execute(java.lang.String, org.springframework.http.HttpMethod, org.springframework.web.client.RequestCallback, - org.springframework.web.client.ResponseExtractor<T>, java.lang.Object...)
-
org.springframework.web.client.RestTemplate#doExecute
image.png
解析response代码
org.springframework.web.client.HttpMessageConverterExtractor#extractData

debug 看到使用的是 StringHttpMessageConvert
StringHttpMessageConvert 解析数据
org.springframework.http.converter.StringHttpMessageConverter#readInternal
@Override
protected String readInternal(Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException {
//1
Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType());
//2
return StreamUtils.copyToString(inputMessage.getBody(), charset);
}
// 1 处是先从response的content-type获取 charset,然后//2出解析字符串,我debug 此处发现response的content-type 没有传入charset,然后使用的charset的是 StandardCharsets.ISO_8859_1。
查看org.springframework.http.converter.StringHttpMessageConverter的构造方法,默认的是ISO_8859_1。
public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
/**
* A default constructor that uses {@code "ISO-8859-1"} as the default charset.
* @see #StringHttpMessageConverter(Charset)
*/
public StringHttpMessageConverter() {
this(DEFAULT_CHARSET);
}
服务A解决乱码的方法
核心是设置StringHttpMessageConverter的charset。
配置RestTemplate
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
RestTemplate restTemplate = new RestTemplate(factory);
List<HttpMessageConverter<?>> messageConverterList = restTemplate.getMessageConverters();
Iterator<HttpMessageConverter<?>> iterator = messageConverterList.iterator();
while ( iterator.hasNext()){
HttpMessageConverter<?> converter = iterator.next();
// 原有的String是ISO-8859-1编码 ,设置为UTF-8
if (converter instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) converter).setDefaultCharset(StandardCharsets.UTF_8);
//iterator.remove();
break;
}
}
return restTemplate;
}
到这里已经可以解决问题,但是为什么服务B升级后会导致乱码的,看服务A的源码可以猜测,服务B升级前返回的content-type 里面有charset,而升级后没有了,导致StringHttpMessageConverter使用的默认的charset。
可以看到,很简单,只是一个分页查询,下面我们来看看如何输出 response的。
B服务的源码解析
升级前的源码
查看选择了哪个HttpMessageConvert
org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)

选择的是org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
看看具体write方法
org.springframework.http.converter.AbstractGenericHttpMessageConverter#write

contentTypeToUse 为application/json ,但是没有设置charset,使用默认的charset,此时默认的
默认的charset是UTF-8,所以response header里的content-type 是application/json;charset=UTF-8。
看一下这个AbstractGenericHttpMessageConverter 的default charset什么时候设置的
在 方法初始化设置的 org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#init
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) {
super(supportedMediaTypes);
init(objectMapper);
}
protected void init(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
// 设置默认的charset
setDefaultCharset(DEFAULT_CHARSET);
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter();
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
this.ssePrettyPrinter = prettyPrinter;
}
升级后的源码
升级spring版本后 处理流程基本类似,核心还是看如何设置response header的 content-type字段的。
org.springframework.http.converter.AbstractHttpMessageConverter#addDefaultHeaders
升级后的default charset为null,所以content-type 未设置charset。

看了一下 AbstractJackson2HttpMessageConverter,发现没有去除了init方法,所以没有设置默认的charset。
总结
Web services, Distributed Objects, etc
Most have same basic structure:
Read request
Decode request
Process service
Encode reply
Send reply
大多数Web服务 具有相同的基本结构,读取请求,解码请求,请求服务,编码回复,发送回复。
springmvc 请求都是使用HttpMessageConvert 编解码,找到对应的HttpMessageConvert,并深入其中的write和read方法,你就可以找到问题的源头。
网友评论