最近在使用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请求的基础抽象类:HttpRequestBaseHttpEntityEnclosingRequestBase,前者扩展的不能传递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即可:

扩展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);
    }
}
HttpEntityEnclosingRequestBase
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,搞定!


相关阅读