logo头像

👨‍💻冷锋のIT小屋

使用Spring的RestTemplate发送GET请求,并支持传递Request body参数

最近在使用Spring Boot实现微服务,都是使用RESTful风格的Api接口,服务间使用RestTemplate来进行HTTP通信,遇到这样一个需求:开发一个查询请求Api,参数使用JSON格式的字符串来提交。

1. 请求格式

希望的请求格式如下:

1
2
3
4
5
6
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:

1
2
3
4
5
6
@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来测试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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,而后者可以。

看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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,该类定义如下:

1
2
3
public class HttpGet extends HttpRequestBase {
……
}

它扩展与HttpRequestBase,当然不能发送body数据,所以我们只需自定义一个factory,扩展HttpComponentsClientHttpRequestFactory即可:

扩展HttpComponentsClientHttpRequestFactory
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
7
8
9
10
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:

1
2
3
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(new HttpComponentsClientRestfulHttpRequestFactory());
return restTemplate;

ok,搞定!

支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励