前言
在项目开发中不可避免的会调用第三方接口,通常是采用httpclient或者okhttp发起请求并处理结果,一般的我们都是封装好对应的工具类发起请求。
事实上,Spring已经为我们提供了一种http请求工具RestTemplate
,因此,本文将重点介绍RestTemplate
的用法及对应的注意事项。
用法
先介绍一下RestTemplate
里的一些基础概念:
- HttpMethod
请求方法类型,该类是一个枚举类,取值为GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
,分别对应一种http请求方法。
大部分场景下,使用GET
和POST
就足够了。 - HttpHeaders
请求头,通过该类在请求时增加对应的请求头。 - HttpEntity
http实体类,该类中具有两个字段headers
和body
。该类具有两个子类RequestEntity
和ResponseEntity
- RequestEntity
请求实体类,该类继承了HttpEntity
,其中还声明了method
、url
和type
三种属性 - ResponseEntity
请求响应实体,通过该实体获取响应状态及对应的响应结果。
RestTemplate
核心Api如下所示:
- getForEntity
看一下接口定义public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
,接口返回值是ResponseEntity
,其中的T
表示影响结果响应体的类型,uriVariables
是一个可变参数,用于在发起请求时替换url
中的占位符。
get请求并获取响应实体
private final String url = "http://127.0.0.1:8050?sign={1}&nonce={2}";
/**
* 实际上应该是由工厂创建
*/
private final RestTemplate restTemplate = new RestTemplate();
@Test
public void getForEntity() {
String sign = "this is sign";
String nonce = "this is nonce";
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, sign, nonce);
//获取响应结果中的数据
String data = responseEntity.getBody();
}
当然,url
中的占位符参数也可以用Map
传入,但此时url
中的占位符要与 Map
中的key一一对应
Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result = restTemplate.getForObject(
"https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
- getForObject
接口定义为public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
,该接口与getForEntity
接口不同的是,能够直接获取到响应结果中的数据
String sign = "this is sign";
String nonce = "this is nonce";
//直接获取响应结果
String data = restTemplate.getForObject(url, String.class, sign, nonce);
- postForEntity
接口定义为public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Object... uriVariables)
,接口返回值是ResponseEntity
,其中的T
表示响应结果类型,request
表示请求体,uriVariables
是一个可变参数,用于在发起请求时替换url
中的占位符。
用法如下所示:
String sign = "this is sign";
String nonce = "this is nonce";
JSONObject request = new JSONObject();
request.put("contractId", "1112358");
request.put("staff", "staff");
ResponseEntity<JSONObject> responseEntity = restTemplate.postForEntity(url, request, JSONObject.class, sign, nonce);
JSONObject data = responseEntity.getBody();
//解析data,判断是否执行成功并获取结果
- postForObject
接口定义为public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
,该接口与postForEntity
接口不同的是,能够直接获取响应结果中的数据。
String sign = "this is sign";
String nonce = "this is nonce";
JSONObject request = new JSONObject();
request.put("contractId", "1112358");
request.put("staff", "staff");
JSONObject data = restTemplate.postForObject(url, request, JSONObject.class, sign, nonce);
//解析data,判断是否执行成功并获取结果
- exchange
方法定义为public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
,通过该方法能够在发起请求时指定请求头。在一些需要登录凭证(将登录后的token放在请求头中)才能调用的接口可以通过该方法调用。
String sign = "this is sign";
String nonce = "this is nonce";
JSONObject request = new JSONObject();
request.put("contractId", "1112358");
request.put("staff", "staff");
//添加请求头
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Token", "this is token");
//构造请求实体
HttpEntity<JSONObject> requestEntity = new HttpEntity<>(request, headers);
ResponseEntity<JSONObject> responseEntity =
restTemplate.exchange(url, HttpMethod.POST, requestEntity, JSONObject.class, sign, nonce);
//获取响应结果并处理
JSONObject data = responseEntity.getBody();
除上述几个方法之外, RestTemplate
内还封装了其他的方法,大部分都是以上方法的重载方法,有兴趣的同学可以看一看源码。
在springboot项目中使用RestTemplate
在springboot项目中,可以在项目启动时创建一个RestTemplate
实例并添加到spring容器中,在使用时直接从容器中注入该实例即可,无需重复创建RestTemplate
对象。
配置如下:
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate(new HttpComponentsClientHttpRequestFactory());
}
}
在创建RestTemplate
对象时,可以指定一个参数ClientHttpRequestFactory
创建连接的工厂。
一般常用的工厂实现有
- Apache HttpComponents(httpclient)
- Netty
- OkHttp
- SimpleClientHttpRequestFactory 使用jdk java.net包内对应的类作为http连接的实现。
由此可以看出,RestTemplate只是一种更高层级的http请求工具,其底层实际发出请求时可以借助各种第三方http连接工厂实现。当然,Spring提供的连接工厂实现远不止以上四种,在具体项目中根据实际情况指定连接工厂的实现类。
使用时的注意事项
使用RestTemplate时,一定要注意的是,RestTemplate会对url进行一次encode,大部分场景下我们传入的url是一个字符串(虽然最后也会被转换成URI)而不是URI,不正确的使用url可能会导致嗲用失败。说一下我遇到的场景:
在调用第三方接口时,一般都会有验签的步骤,签名的生成步骤一般如下:
- 根据appkey、appid、时间戳和其他参数经过RSA算法生成结果1.
- 将结果1经过Base64编码形成结果2.
- 将结果2进行Url Encode形成签名.
我一开始是这么发起请求的
发起请求后,第三方接口返回 签名长度不正确,一开始认为是生成签名的方法不对,但是这个生成签名的方法一直都在被使用,而且使用该接口的其他请求都是正常的。
后来跟了一下代码,发现
RestTemplate
底层代码调用的方法代码如下:
@Override
@Nullable
public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
URI expanded = getUriTemplateHandler().expand(url, uriVariables);
return doExecute(expanded, method, requestCallback, responseExtractor);
}
可以看出,如果传入的参数是url(字符串),其内部会使用DefaultUriBuilderFactory
自动将其转换成URI,其转换的核心代码如下:
可以看出在创建URI时,会判断一下当前的Encode类型,如果是
URI_COMPONENT
类型,则会对UriComponents进行一次encode。而在通过构造器创建
RestTemplate
对象时,会调用initUriTemplateHandler
方法,这个方法定义如下:
private static DefaultUriBuilderFactory initUriTemplateHandler() {
DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory();
uriFactory.setEncodingMode(EncodingMode.URI_COMPONENT); // for backwards compatibility..
return uriFactory;
}
而RSAUtils.calcRsaSign
在生成签名时已经经过了Base64和URLEncode,所以判断是RestTemplate
多做了一次Url Encode导致的,最开始的想法是拿到工具类生成的签名之后对其做一次Url DecodeparamsSign = URLDecoder.decode(paramsSign, StandardCharsets.UTF_8)
,但是,调用时还是发现报错 签名长度不正确。
没办法,只能再看RestTemplate
里的encode方法
发现RestTemplate
中使用的encode方法与java.net.URLEncoder
的encode方法逻辑并不一致。
经过测试发现,
工具类生成的经过Base64、URLEncode后的签名为
Iue89wCfvfghC97KqET%2FMggo1S5V1M9LJNG%2BIU1tputR5yGAUVDzBrS5VlaDzEFnmy7Fh7RZBx3dH4PEvE7cibpr09%2FbLDAPSTXxgQfiVXtI7%2FvJIJXi9mW5uniC%2BFRhCBmQBFGZ5aMBKcqiE3SqEhTxUUEzUhgMysXQW%2BeMFNC3OfsFoQHUa0C09zfyvSR3OCGzSx%2BdDUhnXFHuDgTxkmOI9aLRUt8KG77ZJdZEQFuzkj6ssHFzmajF9OIBMYXz%2BLYJg36KR8RQGDt%2Fye54h8zC8qMMFjvG1HEF8XjlSRyxYSS3k1W6s3JqrE5XSr0mbkhR%2BNqFtTC4N7u3mKa3sQ%3D%3D
将该签名经过URL Decode之后
Iue89wCfvfghC97KqET/Mggo1S5V1M9LJNG+IU1tputR5yGAUVDzBrS5VlaDzEFnmy7Fh7RZBx3dH4PEvE7cibpr09/bLDAPSTXxgQfiVXtI7/vJIJXi9mW5uniC+FRhCBmQBFGZ5aMBKcqiE3SqEhTxUUEzUhgMysXQW+eMFNC3OfsFoQHUa0C09zfyvSR3OCGzSx+dDUhnXFHuDgTxkmOI9aLRUt8KG77ZJdZEQFuzkj6ssHFzmajF9OIBMYXz+LYJg36KR8RQGDt/ye54h8zC8qMMFjvG1HEF8XjlSRyxYSS3k1W6s3JqrE5XSr0mbkhR+NqFtTC4N7u3mKa3sQ==
再把decode之后的签名通过RestTemplate
的进行一次encode
Iue89wCfvfghC97KqET/Mggo1S5V1M9LJNG+IU1tputR5yGAUVDzBrS5VlaDzEFnmy7Fh7RZBx3dH4PEvE7cibpr09/bLDAPSTXxgQfiVXtI7/vJIJXi9mW5uniC+FRhCBmQBFGZ5aMBKcqiE3SqEhTxUUEzUhgMysXQW+eMFNC3OfsFoQHUa0C09zfyvSR3OCGzSx+dDUhnXFHuDgTxkmOI9aLRUt8KG77ZJdZEQFuzkj6ssHFzmajF9OIBMYXz+LYJg36KR8RQGDt/ye54h8zC8qMMFjvG1HEF8XjlSRyxYSS3k1W6s3JqrE5XSr0mbkhR+NqFtTC4N7u3mKa3sQ%3D%3D
发现RestTemplate
encode后的签名与Url Encode后的签名并不一致。
RestTemplate
encode时并未对/
和?
进行处理。因此,通过一次Url Decode之后再由RestTemplate
进行url encode的方法是行不通的。
最终的解决方案
事实上,在使用RestTemplate
发起请求时,最好是通过URI指定请求的路径,通过UriComponentsBuilder
构建UriComponents
,构建时可以指定发起请求时不在对URI进行encode。
代码如下:
public URI getUri(@NonNull String method,
@NonNull String rTick,
@NonNull String sign,
@Nullable Map<String, String> paramMap) {
String rawValidUrl = SERVER_HOST + method;
LinkedMultiValueMap<String, String> multiValueMap = new LinkedMultiValueMap<>();
multiValueMap.add("developerId", DEVELOPER_ID);
multiValueMap.add("rtick", rTick);
multiValueMap.add("signType", "rsa");
multiValueMap.add("sign", sign);
if (!ObjectUtils.isEmpty(paramMap)) {
Set<Map.Entry<String, String>> set = paramMap.entrySet();
set.forEach(entry -> {
multiValueMap.add(entry.getKey(), entry.getValue());
});
}
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(rawValidUrl)
.queryParams(multiValueMap);
// 通过UriComponentsBuilder创建URI对象,这样RestTemplate不会自动进行url encode
UriComponents uriComponents = builder.build(true);
return uriComponents.toUri();
}
总结
大部分场景下,使用RestTemplate能够简单高效的实现我们调用第三方接口的需求,但是也要对其底层实现有一定的了解,否则踩到坑了会很浪费时间。
这里附上官方文档,大家可以参考着官方文档理解RestTemplate官方文档