我们知道 Spring 中 HttpClient 请求使用的 RestTemplate 封装的HttpClient;当我在项目中使用 RestTemplate 做 Post 请求时居然出现乱码的情况,既然出现乱码,编码肯定不一样才导致的,接下来我会分析一下出现乱码的请求。
Spring Version
4.2.3.RELEASE
RestTemplate Test 请求
import com.alibaba.fastjson.JSON;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@RestController
public class RestfulApi {
@PostMapping("/post")
public String api(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
parameterMap.forEach((k, v) -> {
System.out.println(String.format("%s - %s", k, StringUtils.join(v, ",")));
});
return JSON.toJSONString(parameterMap);
}
}
Rest 接口
import org.junit.Test;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class RestTemplateTest {
@Test
public void test() {
Map<String, Object> param = new HashMap<>();
param.put("key", "中文");
param.put("key1", 1);
RestTemplate restTemplate = new RestTemplate();
String s = restTemplate.postForObject("http://localhost:8080/post", toMultiValueMap(param), String.class);
System.out.println(s);
}
public static MultiValueMap<String, Object> toMultiValueMap(Map<String, Object> map) {
MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap();
if (map != null) {
map.forEach((k, v) -> {
multiValueMap.add(k, v instanceof File ? new FileSystemResource((File)v) : v);
});
}
return multiValueMap;
}
}
Post Test 测试
- 接口接收到的值
key1 - 1
key - ?? - RestTempate 请求返回值
{"key1":["1"],"key":["??"]}
如果我们换成 GET 请求会是怎么样的?
Get Test 测试
- 接口接收到的值
key - 中文
key1 - 1 - RestTempate 请求返回值
{"key":["中文"],"key1":["1"]}
我们看到 Get 请求居然是没问题的,那我们换成 Get 请求不就好了吗?好是好,但是项目的接口有时会限制请求的方式,这种方式行不通,那我们就看看 RestTemplate 做了什么会让 Post 请求出现乱码。
接下来我会用调试的方式一步一步看看 RestTemplate 是如何发出请求的。
Step 1
@Override
public <T> T postForObject(String url, Object request, Class<T> responseType, Object... uriVariables)
throws RestClientException {
RequestCallback requestCallback = httpEntityCallback(request, responseType);
HttpMessageConverterExtractor<T> responseExtractor =
new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
return execute(url, HttpMethod.POST, requestCallback, responseExtractor, uriVariables);
}
- httpEntityCallback(request, responseType);
我们在看看protected <T> RequestCallback httpEntityCallback(Object requestBody, Type responseType) { return new HttpEntityRequestCallback(requestBody, responseType); }
HttpEntityRequestCallback
实现了什么?
HttpEntityRequestCallback
继承了AcceptHeaderRequestCallback
,AcceptHeaderRequestCallback
实现了RequestCallback
接口,RequestCallback
接口只有一个方法;
既然实现了public interface RequestCallback { void doWithRequest(ClientHttpRequest request) throws IOException; }
RequestCallback
接口,那么就要实现接口中的方法,那我们看 看实现方法中的内容;-
AcceptHeaderRequestCallback
Request callback implementation that prepares the request's accept headers.
设置Request 请求头 accept -
HttpEntityRequestCallback
Request callback implementation that writes the given object to the request stream.
写入Request 对象流具体的源码我就不贴出了,大家可以参考着源码来理解。
这个类中都使用HttpMessageConverter
的实现类来选择具体的ConvertercanRead
canWrite
那么HttpMessageConverter
又是什么?
-
Step 2
public RestTemplate() {
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new ResourceHttpMessageConverter());
this.messageConverters.add(new SourceHttpMessageConverter<Source>());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
if (romePresent) {
this.messageConverters.add(new AtomFeedHttpMessageConverter());
this.messageConverters.add(new RssChannelHttpMessageConverter());
}
if (jackson2XmlPresent) {
this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
}
else if (jaxb2Present) {
this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
}
if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
}
这是RestTemplate 实例化的时候加载的HttpMessageConverter
,converter都有哪些方法呢?
- canRead(Class<?> clazz, MediaType mediaType)
判断当前的Converter是否支持读
- canWrite(Class<?> clazz, MediaType mediaType)
判断当前的Converter是否支持写入
- read(Class<? extends T> clazz, HttpInputMessage inputMessage)
- write(T t, MediaType contentType, HttpOutputMessage outputMessage)
- getSupportedMediaTypes()
Step 3
上面讲了那么多,无法想给你一个概念,RestTemplate是通过HttpMessageConverter对请求写入流;对返回进行读,解析成你指定的返回类型;如何判断写和读,来选择一个合适的c的呢?是不是通过参数类型和请求类型(Content-Type
),我的想法对不对呢?我们来验证一下;
- 参数类型
对应HttpMessageConverter#canRead或者HttpMessageConverter#canWrite的第一个参数
- 请求类型
对应HttpMessageConverter#canRead或者HttpMessageConverter#canWrite的第二个参数
Step 4
我们只做第一步还没进行下去的debug;
执行前的步骤我省略了,因为哪些方法不重要。
这里总共有5步,其中,第2部是我们要关心的;在Step 1
中我们介绍了RequestCallback
的用法了,在发出请求前(第三步),使用合适的HttpMessageConverter
对写入参数。
具体怎么实现的?
我们进入方法RequestCallback#doWithRequest
我们注意三个地方:
- requestType
变量requestType是我们设置的
MultiValueMap
参数,如果忘了可以往前找找
- requestContentType
变量requestContentType是请求头,这个我们可以自定义,由于我们没有定义,所有这里是空
- messageConverter.canWrite(requestType, requestContentType)
看我猜想的不错,HttpMessageConverter就是通过判断参数类型和请求类型来选择一个合适的Converter的;
往下执行,能支持写的HttpMessageConverter是AllEncompassingFormHttpMessageConverter
Step 5
我们发现了合适的HttpMessageConverter
,我们就看这个Converter如何写的;
Write
@Override
@SuppressWarnings("unchecked")
public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// 如果ContentType 不是 FormData 或者 map 中参数的值全是String,会使用 application/x-www-form-urlencoded
// 否则使用 multipart/form-data
if (!isMultipart(map, contentType)) {
writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
}
else {
writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
}
}
private boolean isMultipart(MultiValueMap<String, ?> map, MediaType contentType) {
if (contentType != null) {
return MediaType.MULTIPART_FORM_DATA.includes(contentType);
}
for (String name : map.keySet()) {
for (Object value : map.get(name)) {
if (value != null && !(value instanceof String)) {
return true;
}
}
}
return false;
}
-
application/x-www-form-urlencoded
该方式使用参数拼接,并且使用的是默认的UTF-8编码,转换的字节;
final byte[] bytes = builder.toString().getBytes(charset.name());
charset 是默认的编码
使用 UTF-8 编码,接收方使用的也是UTF-8,就不会出现乱码的请求;
我们的测试不是使用的这种方式,因为我们的参数中有基本类型;
但是这种方式既然没有问题,我们可以把参数全部设置成String类型的,就不会出现POST请求出现乱码的请情况;但是实际的项目中参数类型不可能全是String的。 -
multipart/form-data
这种方式我们发现它自己内部加载了其它的HttpMessageConverter,通过判断参数的类型,来选择具体的Converter;
看看都加载了哪些HttpMessageConverter
- ByteArrayHttpMessageConverter
- StringHttpMessageConverter
- ResourceHttpMessageConverter
- SourceHttpMessageConverter
- MappingJackson2HttpMessageConverter
- MappingJackson2XmlHttpMessageConverter
出现乱码的请求出现在中文中,中文又是一个String类型的,从上面六个HttpMessageConverter
类名上,可以看出StringHttpMessageConverter
是来处理中文的;出现乱码的情况肯定是编码出现了不一致,我们看看StringHttpMessageConverter
的编码是什么?
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");
居然是ISO-8859-1
,终于找到了罪魁祸首;
编码不一致导致乱码的情况。
Step 6
导致乱码的问题我们找打了,但是如何规避或者解决呢?
下面我给出几个解决方案
- 把所有参数都换成String类型
- 因为
AllEncompassingFormHttpMessageConverter
没有提供入口可以替换本类中的HttpMessageConverter
,不像RestTemplate提供了一个可以获取所有HttpMessageConverter
的方法,所有如果参数中有其它类型时,这就无解了。 - 第2中情况其实是框架级别的bug了,既然大家都在使用 Spring 框架,就不可能没有人没有遇到这种情况,Spring 就不可能不会注意到这bug;想到这我们升级一下 Spring 版本,看看 Spring 会在哪个版本解决这个问题;
- 升级到 版本
-
4.2.4.RELEASE
没解决
-
4.2.5.RELEASE
没解决
-
4.2.6.RELEASE
没解决
-
4.2.7.RELEASE
没解决
-
4.2.8.RELEASE
没解决
-
4.2.9.RELEASE
没解决
-
4.3.0.RELEASE +
解决,问题到这个版本才解决,解决的办法就是在
FormHttpMessageConverter
构造函数中加了一个applyDefaultCharset
方法,对每个HttpMessageConverter
重新设置默认编码 UTF-8
-
public FormHttpMessageConverter() {
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
this.partConverters.add(new ByteArrayHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
stringHttpMessageConverter.setWriteAcceptCharset(false);
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter());
applyDefaultCharset();
}
private void applyDefaultCharset() {
for (HttpMessageConverter<?> candidate : this.partConverters) {
if (candidate instanceof AbstractHttpMessageConverter) {
AbstractHttpMessageConverter<?> converter = (AbstractHttpMessageConverter<?>) candidate;
// Only override default charset if the converter operates with a charset to begin with...
if (converter.getDefaultCharset() != null) {
converter.setDefaultCharset(this.charset);
}
}
}
}