1.直接原因
在项目中有使用HTTPClient(apache.httpcomponents 4.5.13)的方式直接调用其他微服务的接口的场景,我们使用了工具类进行封装,其中核心代码如下:
CloseableHttpClient httpClient = createHttpClient();
HttpPost httpPost = new HttpPost(url);
setHeader(httpPost, headerParamsMap, requestConfig);
httpPost.setEntity(new StringEntity(params, Charset.forName("UTF-8")));
CloseableHttpResponse res = null;
try {
if (httpClient != null) {
res = httpClient.execute(httpPost);
result = EntityUtils.toString(res.getEntity());
if (res.getStatusLine().getStatusCode() != 200 && res.getStatusLine().getStatusCode() != 201) {
throw new RuntimeException(result);
}
}
} catch (IOException var18){
...
}
}
在项目paas改造过程中,主要是对响应体的解析这一行代码进行了变动,原来的代码为
result = EntityUtils.toString(res.getEntity(),"utf-8");
改造后的代码为
result = EntityUtils.toString(res.getEntity();
可以看出,区别在于改造后的代码去除了"utf-8"的设置。
查看EntityUtils.toString(...)方法
public static String toString(
final HttpEntity entity, final Charset defaultCharset) throws IOException, ParseException {
Args.notNull(entity, "Entity");
final InputStream instream = entity.getContent();
if (instream == null) {
return null;
}
try {
Args.check(entity.getContentLength() <= Integer.MAX_VALUE,
"HTTP entity too large to be buffered in memory");
int i = (int)entity.getContentLength();
if (i < 0) {
i = 4096;
}
Charset charset = null;
try {
final ContentType contentType = ContentType.get(entity); // 从entity中获取Content-Type
if (contentType != null) {
charset = contentType.getCharset(); // 并获取charset
}
} catch (final UnsupportedCharsetException ex) {
if (defaultCharset == null) {
throw new UnsupportedEncodingException(ex.getMessage());
}
}
if (charset == null) {
charset = defaultCharset;
}
if (charset == null) {
charset = HTTP.DEF_CONTENT_CHARSET; // 如果响应头和本方法的传参均没有指定,charset则为ISO_8859_1
}
final Reader reader = new InputStreamReader(instream, charset); // 使用选定的charset读取响应流。如果这里charset与内容本身的字符集不同,则可能出现乱码!
final CharArrayBuffer buffer = new CharArrayBuffer(i);
final char[] tmp = new char[1024];
int l;
while((l = reader.read(tmp)) != -1) {
buffer.append(tmp, 0, l);
}
return buffer.toString();
} finally {
instream.close();
}
}
如上分析,在HTTP请求中,需要保证字符集的一致
2.问题复现
使用如上工具类测试,EntityUtils.toString(res.getEntity()) 未加上"utf-8",下面三种方式,前面两种均乱码。最后一种正常
@GetMapping("b") // 默认为application/json
public String testb(){
return "测试中文";
}
@GetMapping(value = "c",produces = MediaType.APPLICATION_JSON_VALUE) // 显式配置application/json
public String testc(){
return "测试中文";
}
@GetMapping(value = "d",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String testd(){
return "测试中文";
}
3.修复
- 使用上面第三种方式,缺点:有额外代码量
- 强制使用utf-8编码,可以使用下面的配置或者配置字符集编码过滤器CharacterEncodingFilter,缺点:写业务代码时缺乏灵活性
server:
servlet:
encoding:
enabled: true
force: true
charset: utf-8
4.思考
-
为什么大部分情况下没有考虑charset的配置也能正常运行?
主流浏览器、postman均默认使用utf-8进行解码,无需显示配置
-
此次问题的产生原因?
项目中使用的httpclient默认使用ISO_8859_1
-
使用其他方式进行远程调用,如FeignClient,会出现乱码吗?
feign的几种解码器StringDecoder,JacksonDecoder等均使用默认utf-8,所以不会出现乱码。(当然你可以定制化Decoder)
5.一些特殊情况
- 项目中若包含全局异常捕捉器ErrorHandler,当接口抛出异常被捕获后,即使标注了produces = MediaType.APPLICATION_JSON_UTF8_VALUE,同样会乱码,这种情况可以通过切面的方式手动处理。
@ControllerAdvice(value = { "com.xxx" })
public class DefaultExceptionHandlerProcessedEncodeAdvice implements ResponseBodyAdvice<ServiceData> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
boolean methodProcessedByDefaultExceptionHandler = false;
Optional<? extends Class<?>> aClass = Optional.of(returnType).map(MethodParameter::getMethod).map(Method::getDeclaringClass);
if (aClass.isPresent()){
// 判断接口返回的结果的方法是不是异常捕捉器
methodProcessedByDefaultExceptionHandler = aClass.get().equals(DefaultExceptionHandler.class);
}
return methodProcessedByDefaultExceptionHandler;
}
@Override
public ServiceData beforeBodyWrite(ServiceData body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
// 如果经过了异常捕捉器或其他定制化的需求,可以在这里手动添加charset
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
return body;
}
}
6.一点经验
大部分情况下无需考虑编码的设置,那是因为HTTP请求的执行链路中,大部分客户端、中间件均使用utf-8作为默认字符集。但是不能保证所有情况,可能部分技术栈由于版本等原因未能支持,这种情况需要根据业务需求、开发成本等进行修改