springboot 我爱编程

Spring RestTemplate 请求乱码的问题

2018-04-12  本文已影响2207人  KengG

我们知道 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 测试

如果我们换成 GET 请求会是怎么样的?

Get Test 测试

我们看到 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);
    }
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都有哪些方法呢?

Step 3

上面讲了那么多,无法想给你一个概念,RestTemplate是通过HttpMessageConverter对请求写入流;对返回进行读,解析成你指定的返回类型;如何判断写和读,来选择一个合适的c的呢?是不是通过参数类型和请求类型(Content-Type),我的想法对不对呢?我们来验证一下;

Step 4

我们只做第一步还没进行下去的debug;

执行

执行前的步骤我省略了,因为哪些方法不重要。

这里总共有5步,其中,第2部是我们要关心的;在Step 1 中我们介绍了RequestCallback的用法了,在发出请求前(第三步),使用合适的HttpMessageConverter对写入参数。

具体怎么实现的?

我们进入方法RequestCallback#doWithRequest

实现

我们注意三个地方:

  1. requestType

变量requestType是我们设置的MultiValueMap参数,如果忘了可以往前找找

  1. requestContentType

变量requestContentType是请求头,这个我们可以自定义,由于我们没有定义,所有这里是空

  1. 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;
}

看看都加载了哪些HttpMessageConverter

出现乱码的请求出现在中文中,中文又是一个String类型的,从上面六个HttpMessageConverter类名上,可以看出StringHttpMessageConverter是来处理中文的;出现乱码的情况肯定是编码出现了不一致,我们看看StringHttpMessageConverter的编码是什么?

public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");

居然是ISO-8859-1,终于找到了罪魁祸首;

编码不一致导致乱码的情况。

Step 6

导致乱码的问题我们找打了,但是如何规避或者解决呢?
下面我给出几个解决方案

  1. 把所有参数都换成String类型
  2. 因为AllEncompassingFormHttpMessageConverter没有提供入口可以替换本类中的HttpMessageConverter,不像RestTemplate提供了一个可以获取所有HttpMessageConverter的方法,所有如果参数中有其它类型时,这就无解了。
  3. 第2中情况其实是框架级别的bug了,既然大家都在使用 Spring 框架,就不可能没有人没有遇到这种情况,Spring 就不可能不会注意到这bug;想到这我们升级一下 Spring 版本,看看 Spring 会在哪个版本解决这个问题;
  4. 升级到 版本
    • 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);
            }
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读