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官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容