RestTemplate的用法及使用时的注意事项

前言

在项目开发中不可避免的会调用第三方接口,通常是采用httpclient或者okhttp发起请求并处理结果,一般的我们都是封装好对应的工具类发起请求。
事实上,Spring已经为我们提供了一种http请求工具RestTemplate,因此,本文将重点介绍RestTemplate的用法及对应的注意事项。

用法

先介绍一下RestTemplate里的一些基础概念:

  • HttpMethod
    请求方法类型,该类是一个枚举类,取值为GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE,分别对应一种http请求方法。
    大部分场景下,使用GETPOST就足够了。
  • HttpHeaders
    请求头,通过该类在请求时增加对应的请求头。
  • HttpEntity
    http实体类,该类中具有两个字段headersbody。该类具有两个子类RequestEntityResponseEntity
  • RequestEntity
    请求实体类,该类继承了 HttpEntity,其中还声明了methodurltype三种属性
  • 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可能会导致嗲用失败。说一下我遇到的场景:

在调用第三方接口时,一般都会有验签的步骤,签名的生成步骤一般如下:

  1. 根据appkey、appid、时间戳和其他参数经过RSA算法生成结果1.
  2. 将结果1经过Base64编码形成结果2.
  3. 将结果2进行Url Encode形成签名.

我一开始是这么发起请求的

image.png

发起请求后,第三方接口返回 签名长度不正确,一开始认为是生成签名的方法不对,但是这个生成签名的方法一直都在被使用,而且使用该接口的其他请求都是正常的。
后来跟了一下代码,发现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,其转换的核心代码如下:

image.png

可以看出在创建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方法

image.png

发现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
发现RestTemplateencode后的签名与Url Encode后的签名并不一致。

image.png

image.png

RestTemplateencode时并未对/?进行处理。因此,通过一次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官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容