spring的RestTemplate中文乱码
好多年没有遇到过乱码的问题了,今天一位同事措不及防的报了个中文乱码问题。我的排查思路如下:
检查数据库中数据状态
理由:可能是从数据库中取数据的时候中问乱码了,首先检查这个是因为这个问题的检查很直观。直接看一眼就知道了。顺便确认了下jdbc链接数据库时是否设置了中文支持。
发现数据库存储的数据就是乱码,检查数据库表的字符编码
理由:如果数据库中的字符编码不支持中文,可能会到导致中文乱码问题。这种方式的检查也很直观,使用下面命令检查:1
show variables like 'character%';
发现编码没问题,检查是否远程数据接收的时候就是乱码
理由:因为接口可以正常接收浏览器发送过来的数据,所以有问题可能是远程程序进行数据传输的时候就乱码了。debug跟踪发现远程程序发送的时候数据并不是乱码<其实没有深入跟踪,深入跟踪后可以发现在发送前就已经是乱码了>,但是接收数据的时候就变成乱码了。
远程发送代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public static String getRespStrForUri(RestTemplate restTemplate, String uri, Map<String, String> params, String cookie) {
MultiValueMap<String, Object> postParameters = new LinkedMultiValueMap<>();
postParameters.add("app_key", Constants.authorityAppKey);
if (params != null) {
for (String key : params.keySet()) {
postParameters.add(key, params.get(key));
}
}
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", cookie);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity(postParameters, headers);
String requestURL = Constants.authorityDomain + uri;
try {
Object responseStr = restTemplate.postForObject(requestURL, requestEntity, Object.class);
return responseStr.toString();
} catch (Exception e) {
}
return null;
}
针对上面的问题,给出解决方法
解决方法一,调整后的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public static String getRespStrForUri(RestTemplate restTemplate, String uri, Map<String, Object> params, String cookie){
MultiValueMap<String, String> postParameters = new LinkedMultiValueMap<>();
postParameters.add("app_key", Constants.authorityAppKey);
if(params!=null){
for(String key : params.keySet()){
postParameters.add(key, String.valueOf(params.get(key)));
}
}
HttpHeaders headers = new HttpHeaders();
headers.add("Cookie", cookie);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity(postParameters, headers);
String requestURL = Constants.authorityDomain + uri ;
try {
String responseStr = restTemplate.postForObject(requestURL, requestEntity, String.class);
return responseStr;
} catch (Exception e){
}
return null;
}
如上所示,调整MultiValueMap泛型值类型为String类型,这里为影响数据管道输出形式。具体细节在下面会介绍。
解决方法二,增加一下配置restTemplate对象交给spring管理:
1 | <bean id="httpMessageConverter" class="org.springframework.http.converter.FormHttpMessageConverter"> |
细节说明
如上所示,主要目的就是StringHttpMessageConverter解析器的编码问题。默认编码问题,部分源码如下
1 | /** |
以上两种方式都可以解决乱码问题,下面说下他们的执行刘流程。
MultiValueMap泛型值为Object类型的时候,执行流程见下图
MultiValueMap泛型值为String类型的时候,执行流程见下图
以上,流程分支判断在FormHttpMessageConverter.java中执行的部分源代码如下:
1 | @Override |
解决方案二的因果
如果数据值的类型为Object类型,因为传输的数据中有整数所以会命中”value != null && !(value instanceof String)”这个条件,导致Object值执行writeMultipart方法。
writeMultipart方法关联源码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104private void writeMultipart(MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage)
throws IOException {
byte[] boundary = generateMultipartBoundary();
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
outputMessage.getHeaders().setContentType(contentType);
writeParts(outputMessage.getBody(), parts, boundary);
writeEnd(boundary, outputMessage.getBody());
}
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(boundary, os);
HttpEntity<?> entity = getEntity(part);
writePart(name, entity, os);
writeNewLine(os);
}
}
}
}
@SuppressWarnings("unchecked")
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
HttpOutputMessage multipartOutputMessage = new MultipartHttpOutputMessage(os);
multipartOutputMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody));
if (!partHeaders.isEmpty()) {
multipartOutputMessage.getHeaders().putAll(partHeaders);
}
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartOutputMessage);
return;
}
}
throw new HttpMessageNotWritableException(
"Could not write request: no suitable HttpMessageConverter found for request type [" +
partType.getName() + "]");
}
// messageConverter为StringHttpMessageConverter的时满足条件messageConverter.canWrite(partType, partContentType)
// ((HttpMessageConverter<Object>) messageConverter).write()实例执行为StringHttpMessageConverter.write()
/**
* This implementation delegates to {@link #getDefaultContentType(Object)} if a content
* type was not provided, calls {@link #getContentLength}, and sets the corresponding headers
* on the output message. It then calls {@link #writeInternal}.
*/
@Override
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
if (headers.getContentType() == null) {
if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {
contentType = getDefaultContentType(t);
}
if (contentType != null) {
headers.setContentType(contentType);
}
}
if (headers.getContentLength() == -1) {
Long contentLength = getContentLength(t, headers.getContentType());
if (contentLength != null) {
headers.setContentLength(contentLength);
}
}
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage =
(StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, outputMessage);
outputMessage.getBody().flush();
}
}
@Override
protected void writeInternal(String s, HttpOutputMessage outputMessage) throws IOException {
if (this.writeAcceptCharset) {
outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets());
}
Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType());
StreamUtils.copy(s, charset, outputMessage.getBody());
}
跟踪至此,如果你没有配置上面的解决方法你会发现如下的数据信息:1
2
3
4
5
6
7
8
9
10
11
12outputMessage:
8DFNcO0RxUKPvP42
--I3cKHLAEz25wEPbiBVY15yDrDMI3hvI5xw
Content-Disposition: form-data; name="sequence"
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 3
10
--I3cKHLAEz25wEPbiBVY15yDrDMI3hvI5xw
Content-Disposition: form-data; name="name"
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 13
01_05_???????
没错,此时的中文已经是乱码了。Content-Type编码类型也是默认的ISO-8859-1.到这里解决办法就很容易考虑到了,重写默认编码就可以解决这个问题。这也就是解决方案二的由来。
多说一句因为这个项目是老项目用的还是spring的配置文件管理bean以来,现在新项目都是用了springboot然后根据业务对项目进行划分。对系统中使用的对象都会提前进行相关属性的配置,部分上可以避免类似问题。
解决方案一的因果
参考上面的源码,这里就补贴出来了。因为没有设置contentType,而且值也都转为了String类型所以很明显要执行writeForm方法,writeForm方法源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33private void writeForm(MultiValueMap<String, String> form, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException {
Charset charset;
if (contentType != null) {
outputMessage.getHeaders().setContentType(contentType);
charset = contentType.getCharSet() != null ? contentType.getCharSet() : this.charset;
}
else {
outputMessage.getHeaders().setContentType(MediaType.APPLICATION_FORM_URLENCODED);
charset = this.charset;
}
StringBuilder builder = new StringBuilder();
for (Iterator<String> nameIterator = form.keySet().iterator(); nameIterator.hasNext();) {
String name = nameIterator.next();
for (Iterator<String> valueIterator = form.get(name).iterator(); valueIterator.hasNext();) {
String value = valueIterator.next();
builder.append(URLEncoder.encode(name, charset.name()));
if (value != null) {
builder.append('=');
builder.append(URLEncoder.encode(value, charset.name()));
if (valueIterator.hasNext()) {
builder.append('&');
}
}
}
if (nameIterator.hasNext()) {
builder.append('&');
}
}
byte[] bytes = builder.toString().getBytes(charset.name());
outputMessage.getHeaders().setContentLength(bytes.length);
StreamUtils.copy(bytes, outputMessage.getBody());
}
这个方法的解决方法比较直观builder.append(URLEncoder.encode(value, charset.name()))。
以上。