Springboot 远程调用 乱码的解决

450 阅读1分钟

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.思考

  1. 为什么大部分情况下没有考虑charset的配置也能正常运行?

    主流浏览器、postman均默认使用utf-8进行解码,无需显示配置

  2. 此次问题的产生原因?

    项目中使用的httpclient默认使用ISO_8859_1

  3. 使用其他方式进行远程调用,如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作为默认字符集。但是不能保证所有情况,可能部分技术栈由于版本等原因未能支持,这种情况需要根据业务需求、开发成本等进行修改