前言
在項(xiàng)目開(kāi)發(fā)中不可避免的會(huì)調(diào)用第三方接口餐曹,通常是采用httpclient或者okhttp發(fā)起請(qǐng)求并處理結(jié)果蜜另,一般的我們都是封裝好對(duì)應(yīng)的工具類發(fā)起請(qǐng)求古今。
事實(shí)上蠕搜,Spring已經(jīng)為我們提供了一種http請(qǐng)求工具RestTemplate
轮蜕,因此昨悼,本文將重點(diǎn)介紹RestTemplate
的用法及對(duì)應(yīng)的注意事項(xiàng)。
用法
先介紹一下RestTemplate
里的一些基礎(chǔ)概念:
- HttpMethod
請(qǐng)求方法類型跃洛,該類是一個(gè)枚舉類率触,取值為GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
,分別對(duì)應(yīng)一種http請(qǐng)求方法汇竭。
大部分場(chǎng)景下葱蝗,使用GET
和POST
就足夠了。 - HttpHeaders
請(qǐng)求頭细燎,通過(guò)該類在請(qǐng)求時(shí)增加對(duì)應(yīng)的請(qǐng)求頭两曼。 - HttpEntity
http實(shí)體類,該類中具有兩個(gè)字段headers
和body
找颓。該類具有兩個(gè)子類RequestEntity
和ResponseEntity
- RequestEntity
請(qǐng)求實(shí)體類合愈,該類繼承了HttpEntity
,其中還聲明了method
、url
和type
三種屬性 - ResponseEntity
請(qǐng)求響應(yīng)實(shí)體佛析,通過(guò)該實(shí)體獲取響應(yīng)狀態(tài)及對(duì)應(yīng)的響應(yīng)結(jié)果益老。
RestTemplate
核心Api如下所示:
- getForEntity
看一下接口定義public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
,接口返回值是ResponseEntity
,其中的T
表示影響結(jié)果響應(yīng)體的類型寸莫,uriVariables
是一個(gè)可變參數(shù)捺萌,用于在發(fā)起請(qǐng)求時(shí)替換url
中的占位符。
get請(qǐng)求并獲取響應(yīng)實(shí)體
private final String url = "http://127.0.0.1:8050?sign={1}&nonce={2}";
/**
* 實(shí)際上應(yīng)該是由工廠創(chuàng)建
*/
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);
//獲取響應(yīng)結(jié)果中的數(shù)據(jù)
String data = responseEntity.getBody();
}
當(dāng)然膘茎,url
中的占位符參數(shù)也可以用Map
傳入桃纯,但此時(shí)url
中的占位符要與 Map
中的key一一對(duì)應(yīng)
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
接口不同的是披坏,能夠直接獲取到響應(yīng)結(jié)果中的數(shù)據(jù)
String sign = "this is sign";
String nonce = "this is nonce";
//直接獲取響應(yīng)結(jié)果
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
表示響應(yīng)結(jié)果類型棒拂,request
表示請(qǐng)求體伞梯,uriVariables
是一個(gè)可變參數(shù),用于在發(fā)起請(qǐng)求時(shí)替換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,判斷是否執(zhí)行成功并獲取結(jié)果
- postForObject
接口定義為public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)
谜诫,該接口與postForEntity
接口不同的是,能夠直接獲取響應(yīng)結(jié)果中的數(shù)據(jù)攻旦。
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,判斷是否執(zhí)行成功并獲取結(jié)果
- exchange
方法定義為public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
喻旷,通過(guò)該方法能夠在發(fā)起請(qǐng)求時(shí)指定請(qǐng)求頭。在一些需要登錄憑證(將登錄后的token放在請(qǐng)求頭中)才能調(diào)用的接口可以通過(guò)該方法調(diào)用牢屋。
String sign = "this is sign";
String nonce = "this is nonce";
JSONObject request = new JSONObject();
request.put("contractId", "1112358");
request.put("staff", "staff");
//添加請(qǐng)求頭
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
headers.add("Token", "this is token");
//構(gòu)造請(qǐng)求實(shí)體
HttpEntity<JSONObject> requestEntity = new HttpEntity<>(request, headers);
ResponseEntity<JSONObject> responseEntity =
restTemplate.exchange(url, HttpMethod.POST, requestEntity, JSONObject.class, sign, nonce);
//獲取響應(yīng)結(jié)果并處理
JSONObject data = responseEntity.getBody();
除上述幾個(gè)方法之外且预, RestTemplate
內(nèi)還封裝了其他的方法,大部分都是以上方法的重載方法烙无,有興趣的同學(xué)可以看一看源碼辣之。
在springboot項(xiàng)目中使用RestTemplate
在springboot項(xiàng)目中,可以在項(xiàng)目啟動(dòng)時(shí)創(chuàng)建一個(gè)RestTemplate
實(shí)例并添加到spring容器中皱炉,在使用時(shí)直接從容器中注入該實(shí)例即可,無(wú)需重復(fù)創(chuàng)建RestTemplate
對(duì)象狮鸭。
配置如下:
public class RestTemplateConfiguration {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate(new HttpComponentsClientHttpRequestFactory());
}
}
在創(chuàng)建RestTemplate
對(duì)象時(shí)合搅,可以指定一個(gè)參數(shù)ClientHttpRequestFactory
創(chuàng)建連接的工廠。
一般常用的工廠實(shí)現(xiàn)有
- Apache HttpComponents(httpclient)
- Netty
- OkHttp
- SimpleClientHttpRequestFactory 使用jdk java.net包內(nèi)對(duì)應(yīng)的類作為http連接的實(shí)現(xiàn)歧蕉。
由此可以看出灾部,RestTemplate只是一種更高層級(jí)的http請(qǐng)求工具,其底層實(shí)際發(fā)出請(qǐng)求時(shí)可以借助各種第三方http連接工廠實(shí)現(xiàn)惯退。當(dāng)然赌髓,Spring提供的連接工廠實(shí)現(xiàn)遠(yuǎn)不止以上四種,在具體項(xiàng)目中根據(jù)實(shí)際情況指定連接工廠的實(shí)現(xiàn)類。
使用時(shí)的注意事項(xiàng)
使用RestTemplate時(shí)锁蠕,一定要注意的是夷野,RestTemplate會(huì)對(duì)url進(jìn)行一次encode,大部分場(chǎng)景下我們傳入的url是一個(gè)字符串(雖然最后也會(huì)被轉(zhuǎn)換成URI)而不是URI荣倾,不正確的使用url可能會(huì)導(dǎo)致嗲用失敗悯搔。說(shuō)一下我遇到的場(chǎng)景:
在調(diào)用第三方接口時(shí),一般都會(huì)有驗(yàn)簽的步驟舌仍,簽名的生成步驟一般如下:
- 根據(jù)appkey妒貌、appid、時(shí)間戳和其他參數(shù)經(jīng)過(guò)RSA算法生成結(jié)果1.
- 將結(jié)果1經(jīng)過(guò)Base64編碼形成結(jié)果2.
- 將結(jié)果2進(jìn)行Url Encode形成簽名.
我一開(kāi)始是這么發(fā)起請(qǐng)求的
發(fā)起請(qǐng)求后铸豁,第三方接口返回 簽名長(zhǎng)度不正確灌曙,一開(kāi)始認(rèn)為是生成簽名的方法不對(duì),但是這個(gè)生成簽名的方法一直都在被使用节芥,而且使用該接口的其他請(qǐng)求都是正常的在刺。
后來(lái)跟了一下代碼,發(fā)現(xiàn)
RestTemplate
底層代碼調(diào)用的方法代碼如下:
@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);
}
可以看出藏古,如果傳入的參數(shù)是url(字符串)增炭,其內(nèi)部會(huì)使用DefaultUriBuilderFactory
自動(dòng)將其轉(zhuǎn)換成URI,其轉(zhuǎn)換的核心代碼如下:
可以看出在創(chuàng)建URI時(shí)拧晕,會(huì)判斷一下當(dāng)前的Encode類型隙姿,如果是
URI_COMPONENT
類型,則會(huì)對(duì)UriComponents進(jìn)行一次encode厂捞。而在通過(guò)構(gòu)造器創(chuàng)建
RestTemplate
對(duì)象時(shí)输玷,會(huì)調(diào)用initUriTemplateHandler
方法,這個(gè)方法定義如下:
private static DefaultUriBuilderFactory initUriTemplateHandler() {
DefaultUriBuilderFactory uriFactory = new DefaultUriBuilderFactory();
uriFactory.setEncodingMode(EncodingMode.URI_COMPONENT); // for backwards compatibility..
return uriFactory;
}
而RSAUtils.calcRsaSign
在生成簽名時(shí)已經(jīng)經(jīng)過(guò)了Base64和URLEncode靡馁,所以判斷是RestTemplate
多做了一次Url Encode導(dǎo)致的欲鹏,最開(kāi)始的想法是拿到工具類生成的簽名之后對(duì)其做一次Url DecodeparamsSign = URLDecoder.decode(paramsSign, StandardCharsets.UTF_8)
,但是臭墨,調(diào)用時(shí)還是發(fā)現(xiàn)報(bào)錯(cuò) 簽名長(zhǎng)度不正確赔嚎。
沒(méi)辦法,只能再看RestTemplate
里的encode方法
發(fā)現(xiàn)RestTemplate
中使用的encode方法與java.net.URLEncoder
的encode方法邏輯并不一致胧弛。
經(jīng)過(guò)測(cè)試發(fā)現(xiàn)尤误,
工具類生成的經(jīng)過(guò)Base64、URLEncode后的簽名為
Iue89wCfvfghC97KqET%2FMggo1S5V1M9LJNG%2BIU1tputR5yGAUVDzBrS5VlaDzEFnmy7Fh7RZBx3dH4PEvE7cibpr09%2FbLDAPSTXxgQfiVXtI7%2FvJIJXi9mW5uniC%2BFRhCBmQBFGZ5aMBKcqiE3SqEhTxUUEzUhgMysXQW%2BeMFNC3OfsFoQHUa0C09zfyvSR3OCGzSx%2BdDUhnXFHuDgTxkmOI9aLRUt8KG77ZJdZEQFuzkj6ssHFzmajF9OIBMYXz%2BLYJg36KR8RQGDt%2Fye54h8zC8qMMFjvG1HEF8XjlSRyxYSS3k1W6s3JqrE5XSr0mbkhR%2BNqFtTC4N7u3mKa3sQ%3D%3D
將該簽名經(jīng)過(guò)URL Decode之后
Iue89wCfvfghC97KqET/Mggo1S5V1M9LJNG+IU1tputR5yGAUVDzBrS5VlaDzEFnmy7Fh7RZBx3dH4PEvE7cibpr09/bLDAPSTXxgQfiVXtI7/vJIJXi9mW5uniC+FRhCBmQBFGZ5aMBKcqiE3SqEhTxUUEzUhgMysXQW+eMFNC3OfsFoQHUa0C09zfyvSR3OCGzSx+dDUhnXFHuDgTxkmOI9aLRUt8KG77ZJdZEQFuzkj6ssHFzmajF9OIBMYXz+LYJg36KR8RQGDt/ye54h8zC8qMMFjvG1HEF8XjlSRyxYSS3k1W6s3JqrE5XSr0mbkhR+NqFtTC4N7u3mKa3sQ==
再把decode之后的簽名通過(guò)RestTemplate
的進(jìn)行一次encode
Iue89wCfvfghC97KqET/Mggo1S5V1M9LJNG+IU1tputR5yGAUVDzBrS5VlaDzEFnmy7Fh7RZBx3dH4PEvE7cibpr09/bLDAPSTXxgQfiVXtI7/vJIJXi9mW5uniC+FRhCBmQBFGZ5aMBKcqiE3SqEhTxUUEzUhgMysXQW+eMFNC3OfsFoQHUa0C09zfyvSR3OCGzSx+dDUhnXFHuDgTxkmOI9aLRUt8KG77ZJdZEQFuzkj6ssHFzmajF9OIBMYXz+LYJg36KR8RQGDt/ye54h8zC8qMMFjvG1HEF8XjlSRyxYSS3k1W6s3JqrE5XSr0mbkhR+NqFtTC4N7u3mKa3sQ%3D%3D
發(fā)現(xiàn)RestTemplate
encode后的簽名與Url Encode后的簽名并不一致结缚。
RestTemplate
encode時(shí)并未對(duì)/
和?
進(jìn)行處理损晤。因此,通過(guò)一次Url Decode之后再由RestTemplate
進(jìn)行url encode的方法是行不通的红竭。
最終的解決方案
事實(shí)上尤勋,在使用RestTemplate
發(fā)起請(qǐng)求時(shí)喘落,最好是通過(guò)URI指定請(qǐng)求的路徑,通過(guò)UriComponentsBuilder
構(gòu)建UriComponents
最冰,構(gòu)建時(shí)可以指定發(fā)起請(qǐng)求時(shí)不在對(duì)URI進(jìn)行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);
// 通過(guò)UriComponentsBuilder創(chuàng)建URI對(duì)象,這樣RestTemplate不會(huì)自動(dòng)進(jìn)行url encode
UriComponents uriComponents = builder.build(true);
return uriComponents.toUri();
}
總結(jié)
大部分場(chǎng)景下锌奴,使用RestTemplate能夠簡(jiǎn)單高效的實(shí)現(xiàn)我們調(diào)用第三方接口的需求兽狭,但是也要對(duì)其底層實(shí)現(xiàn)有一定的了解,否則踩到坑了會(huì)很浪費(fèi)時(shí)間鹿蜀。
這里附上官方文檔箕慧,大家可以參考著官方文檔理解RestTemplate官方文檔