RestTemplate的用法及使用時(shí)的注意事項(xiàng)

前言

在項(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)景下葱蝗,使用GETPOST就足夠了。
  • HttpHeaders
    請(qǐng)求頭细燎,通過(guò)該類在請(qǐng)求時(shí)增加對(duì)應(yīng)的請(qǐng)求頭两曼。
  • HttpEntity
    http實(shí)體類,該類中具有兩個(gè)字段headersbody找颓。該類具有兩個(gè)子類RequestEntityResponseEntity
  • RequestEntity
    請(qǐng)求實(shí)體類合愈,該類繼承了 HttpEntity,其中還聲明了methodurltype三種屬性
  • 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)簽的步驟舌仍,簽名的生成步驟一般如下:

  1. 根據(jù)appkey妒貌、appid、時(shí)間戳和其他參數(shù)經(jīng)過(guò)RSA算法生成結(jié)果1.
  2. 將結(jié)果1經(jīng)過(guò)Base64編碼形成結(jié)果2.
  3. 將結(jié)果2進(jìn)行Url Encode形成簽名.

我一開(kāi)始是這么發(fā)起請(qǐng)求的

image.png

發(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)換的核心代碼如下:

image.png

可以看出在創(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方法

image.png

發(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)RestTemplateencode后的簽名與Url Encode后的簽名并不一致结缚。

image.png

image.png

RestTemplateencode時(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官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市茴恰,隨后出現(xiàn)的幾起案子颠焦,更是在濱河造成了極大的恐慌,老刑警劉巖往枣,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伐庭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡分冈,警方通過(guò)查閱死者的電腦和手機(jī)圾另,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)雕沉,“玉大人集乔,你說(shuō)我怎么就攤上這事∑陆罚” “怎么了扰路?”我有些...
    開(kāi)封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)倔叼。 經(jīng)常有香客問(wèn)我汗唱,道長(zhǎng),這世上最難降的妖魔是什么丈攒? 我笑而不...
    開(kāi)封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任哩罪,我火速辦了婚禮,結(jié)果婚禮上巡验,老公的妹妹穿的比我還像新娘识椰。我一直安慰自己,他們只是感情好深碱,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著藏畅,像睡著了一般敷硅。 火紅的嫁衣襯著肌膚如雪功咒。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天绞蹦,我揣著相機(jī)與錄音力奋,去河邊找鬼。 笑死幽七,一個(gè)胖子當(dāng)著我的面吹牛景殷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播澡屡,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼猿挚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了驶鹉?” 一聲冷哼從身側(cè)響起绩蜻,我...
    開(kāi)封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎室埋,沒(méi)想到半個(gè)月后办绝,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姚淆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年孕蝉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腌逢。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡降淮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出上忍,到底是詐尸還是另有隱情骤肛,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布窍蓝,位于F島的核電站腋颠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏吓笙。R本人自食惡果不足惜淑玫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望面睛。 院中可真熱鬧絮蒿,春花似錦、人聲如沸叁鉴。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)幌墓。三九已至但壮,卻和暖如春冀泻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蜡饵。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工弹渔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人溯祸。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓肢专,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親焦辅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子博杖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容