最近在使用Spring Boot实现微服务,都是使用RESTful风格的Api接口,服务间使用RestTemplate来进行HTTP通信,遇到这样一个需求:开发一个查询请求Api,参数使用JSON格式的字符串来提交。
1. 请求格式
希望的请求格式如下:
GET /pointCard/ HTTP/1.1
Host: localhost:8100
Content-Type: application/json;charset=UTF-8
Content-Length: 114
{"iColumns":7,"iDisplayLength":10,"iDisplayStart":0,"iSortingCols":0,"sColumns":"","sEcho":1,"subjectId":"11227"}在RESTful下,这样的设计是合理的,GET请求表示从服务器获取资源,但需要将查询参数以JSON格式来提交。但是,这违背了传统的GET请求的规范,我们都知道,GET请求只能将请求参数拼接URI后边,而不能单独传递request body参数,除非你改用POST。
2. 代码实现
我们先来编一个上述请求的API,然后进行测试。
1、编写一个API:
@GetMapping(value = "/")
public Response getById(@RequestBody @Valid PointCardQuery query) throws Exception {
    Assert.notNull(query,"查询条件不能为空!");
    ……
    return Response.success(pointCardPurePager, "积分卡获取成功!");
}上边的代码片段处于一个Restcontroller,要求使用GET方法,并且使用了@RequestBody注解来获取request body参数。
2、我们使用RestTemplate来测试一下:
@Test
public void testGetWithBody() {
    RestTemplate restTemplate = new RestTemplate();
    String p = "{\"iColumns\":7,\"iDisplayLength\":10,\"iDisplayStart\":0,\"iSortingCols\":0,\"sColumns\":\"\",\"sEcho\":1,\"subjectId\":\"11227\"}";
    String url = "http://localhost:8100/pointCard/";
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);
    headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
    HttpEntity<String> httpEntity = new HttpEntity<>(p, headers);
    ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, httpEntity, String.class);
    String body = responseEntity.getBody();
    System.out.println(body);
    System.out.println(responseEntity.getStatusCode());
    System.out.println(responseEntity.getStatusCodeValue());
    System.out.println(responseEntity);
}运行测试代码,发现请求直接400错误:
org.springframework.web.client.HttpClientErrorException: 400 null
    at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:85)
    ……查阅了大量资料,大部分都是说GET请求不能传递Request body,对于RESTful而言,这显然是不合理的。
记得原来开发ElasticSearch的时候,很多API都是这样的形式:
GET /_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title":          "quick brown fox" }},
        { "match": { "title.original": "quick brown fox" }},
        { "match": { "title.shingles": "quick brown fox" }}
      ]
    }
  }
}当时还是用了curl工具来测试其API,何不试试?
3、使用curl测试:
~> curl -XGET -k "http://localhost:8100/pointCard/" \
    --include \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    --data '{"iColumns":7,"iDisplayLength":10,"iDisplayStart":0,"iSortingCols":0,"sColumns":"","sEcho":1,"subjectId":"11227"}'出乎意料,curl可以正常工作。看来,问题出在测试代码的RestTemplate上。
3. 解决方案
继续查询资料,一篇Meik Kaufmann的文章解决了我的问题,地址在文末,他遇到的问题跟我所遇到的非常相似 [1]。
其实,在HTTP1.1中,任何请求都可以发送body数据,只是Spring的RestTemplate默认使用JDK的HTTP请求实现:
by default the RestTemplate relies on standard JDK facilities to establish HTTP connections. You can switch to use a different HTTP library such as Apache HttpComponents, Netty, and OkHttp through the setRequestFactory property.
RestTemplate支持通过setRequestFactory设置HTTP请求客户端工具,支持jdk、httpclient、okHttp等,默认使用的是SimpleClientHttpRequestFactory,该工程使用的JDK实现,底层使用OutputStream来传递body数据,不支持GET传递body。
我们可以修改为httpclient,只需要使用HttpComponentsClientHttpRequestFactory,但是默认的httpclient的GET请求也是不支持传递body的。有两个用于定义Http请求的基础抽象类:HttpRequestBase、HttpEntityEnclosingRequestBase,前者扩展的不能传递body,而后者可以。
看如下代码:
protected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
    switch (httpMethod) {
        case GET:
            return new HttpGet(uri);
        case HEAD:
            return new HttpHead(uri);
        case POST:
            return new HttpPost(uri);
        case PUT:
            return new HttpPut(uri);
        case PATCH:
            return new HttpPatch(uri);
        case DELETE:
            return new HttpDelete(uri);
        case OPTIONS:
            return new HttpOptions(uri);
        case TRACE:
            return new HttpTrace(uri);
        default:
            throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
    }
}GET请求使用的是HttpGet,该类定义如下:
public class HttpGet extends HttpRequestBase {
    ……
}它扩展与HttpRequestBase,当然不能发送body数据,所以我们只需自定义一个factory,扩展HttpComponentsClientHttpRequestFactory即可:
private static final class HttpComponentsClientRestfulHttpRequestFactory extends HttpComponentsClientHttpRequestFactory {
    @Override
    protected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
        if (httpMethod == HttpMethod.GET) {
            return new HttpGetRequestWithEntity(uri);
        }
        return super.createHttpUriRequest(httpMethod, uri);
    }
}private static final class HttpGetRequestWithEntity extends HttpEntityEnclosingRequestBase {
    public HttpGetRequestWithEntity(final URI uri) {
        super.setURI(uri);
    }
    @Override
    public String getMethod() {
        return HttpMethod.GET.name();
    }
}自定义扩展很简单,所以的GET请求都使用扩展的HttpGetRequestWithEntity对象,这样就可以传递body了。
然后在定义RestTemplate时,使用自定义factory:
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientRestfulHttpRequestFactory());
return restTemplate;ok,搞定!