关于远程调用RestTemplate的使用避坑指南
目录
- 一、前言介绍
- 二、 问题记录
- 1. 慎!【url参数中有json字符串】
- 2. 慎!【url参数中有经过URLEncode的字符串】
- 3. 慎!【url参数中存在特殊字符】 --- 针对HttpClient
- 总结
一、前言介绍
RestTemplate是Spring中用于远程接口调用的工具类,它是Apache的HttpClient的模板封装,使用起来非常方便,本文将讲述这两天自己在使用RestTemplate过程中遇到的问题,当然这些问题也是由于自己对RestTemplate工具类了解不够全面不够透彻造成的,希望自己遇到的这些问题能为大家提前避雷或是遇到类似问题时的一个解决参考。
二、 问题记录
1. 慎!【url参数中有json字符串】
在使用RestTemplate进行远程接口调用时,如果url拼接参数中json字符串时一定要小心,使用场景如下:利用restTemplate调用user的查询信息接口,url中的一个参数user为json字符串格式{\"user\":\"xiaoming,\"age\":"12"}
// JSON参数 Map<String, String> paramMap = new HashMap<>(8); paramMap.put("name","xiaoming"); paramMap.put("age","12"); String paramJsonStr = JSONObject.toJSONString(paramMap); // 实际参数 url = "http://localhost:8080/api/user?user={\"name\":\"xiaoming\",\"age\":\"12\"}&country=china"; String url = "http://localhost:8080/api/user?user=" + paramJsonStr + "&country=china"; RestTemplate restTemplate = new RestTemplate(); // 调用出错 Object execute = restTemplate.execute(url, HttpMethod.GET, null, null);
此时当我们运行程序时会抛出以下错误:
错误意思大概是没有足够可用的变量值来填充扩展 'name',这是什么鬼意思,别着急让我们跟跟代码看看异常抛出的位置,最终定位如下,在创建URI过程中调用了 UriComponents.expandUriComponent()方法抛出异常:
这段代码的作用其实就是通过NAMES_PATERN规则匹配到相应字符串然后利用 uriVariables.getValue(varibaleName)进行替换,再看看NAMES_PATERN的值就是用来匹配{}中的字符串内容的
private static final Pattern NAMES_PATTERN = Pattern.compile(\\{([^/]+?)\\});
问题分析到这儿相信大家应该也明白了RestTemplate在创建URI时会进行{param}占位替换,这个规则在文本输出时应用比较多,如日志打印和控制台打印中常有使用:
String value = "test"; logger.info("占位参数{}",value);
解决办法:
找到原因了,那么我们应当如何解决呢,既然RestTemplate在处理url时会进行{}变量替换,那它理应提供相应的接口调用,查看RestTemplate源码它提供了多个exchange重载方法,其中多个方法都有uriVariables参数,如下所示:
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException { URI expanded = this.getUriTemplateHandler().expand(url, uriVariables); return this.doExecute(expanded, method, requestCallback, responseExtractor); }
那么我们将url稍微修改即可解决问题:
2. 慎!【url参数中有经过URLEncode的字符串】
其实在遇到第一个坑时,我并没有采用上面给出的解决方式,而是想着将Json字符串经过URLEncode编码后在拼接到url后面,不就没有{}符号了,不就可以完美解决问题了,心里想着就美滋滋,那让我们来试一把吧:
// JSON参数 Map<String, String> paramMap = new HashMap<>(8); paramMap.put("name","xiaoming"); paramMap.put("age","12"); String paramJsonStr = JSONObject.toJSONString(paramMap); // 实际参数 url = "http://localhost:8080/api/user?user=%7B%22name%22%3A%22xiaoming%22%2C%22age%22%3A%2212%22%7D&country=china"; String encode = URLEncoder.encode(paramJsonStr, "utf-8"); String url = "http://localhost:8080/api/user?user="+encode+"&country=china"; System.out.println(url); RestTemplate restTemplate = new RestTemplate(); Object execute = restTemplate.execute(url, HttpMethod.GET, null, null,paramJsonStr);
json字符串经过编码后已经没有{}符号了,也能够成功调用接口,但是接口提供方无情的返回了错误:参数无法反序列化。听这口气肯定也是这json串的原因,赶紧用Postman试一试:
神奇的一幕出现了,居然成功了! Postman方式调用和RestTemplate调用有什么不一样,为什么postman行,restTemplate不行?赶紧查看服务器日志看看两种方式接收到的参数有和不一样:
1. Postman方式服务器接收到的url:
"http://localhost:8080/api/user?user=%7B%22name%22%3A%22xiaoming%22%2C%22age%22%3A%2212%22%7D&country=china"
2. RestTemplate方式服务器接收到的url
"http://localhost:8080/api/user?user=%257B%2522name%2522%3A%2522xiaoming%2522%2C%2522age%2522%253A%252212%2522%257D&country=china"
restTemplate居然在每个百分号%后面都擅自加了25这个数字,难怪服务端没法解析,它为什么要这么做?难道是restTemplate的url处理bug?让我们跟一跟代码看个究竟:
详细调用层次就不贴了,简单来说就是RestTemplate在处理url时会对url参数进行再编码,也就是会对url中的特殊字符进行转义,如%号会被转义为%25,所以传给服务端的url就被改变了,具体url特殊字符转义知识请查看这篇文章
既然知道了原因,那么我们应该如何解决呢?
解决方案:
RestTemplate中的URI对象是通过UriTemplateHandler生成的,所以我们只需要利用java.net包中的URI自己构建URI对象传给RestTemplate即可,这样url中的特殊字符就不会被转义了:
3. 慎!【url参数中存在特殊字符】 --- 针对HttpClient
前面两个坑然我对RestTemplate有点望而生畏了,既然RestTemplate这么多坑,那好咋们换回老家伙apache家族的HttpClient,本以为可以一切顺利,没成想一坑接着一坑啊!!!
先看看调用场景:按照出生时间去查询用户信息
调用请求都还没发出就无情报错了:
通过异常信息可以容易知道在创建URL对象时url参数索引位置50处有非法字符,而这个字符刚好就是时间参数中的空格!,跟踪代码异常发生位置发现下面一段注释,大意就是不允许url中有特殊字符存在,看了本文第二个坑的应该已经明白url中特殊字符为什么需要进行转义了,这里就不详细叙述,至此解决方案就呼之欲出了。
解决方案:
需要对参数中的特殊字符进行转义:
1. 直接特殊字符替换
2. 利用google的工具包UrlEscapers(可以处理url、xml、html中的特殊字符)
maven依赖
<!-- https://mvnrepository.com/artifact/com.google.guava/guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>28.1-jre</version> </dependency>
使用方式:UrlEscapers可以对路径、参数、片段进行处理,提供了 path,parameter,fragment三个部分的Escape实例
分别调用UrlEscapers类的以下方法获取:
urlFormParameterEscaper()
urlPathSegmentEscaper()
urlFragmentEscaper()
总结
本次三个案例本人觉得还是具有典型性,由于平时发起请求大多通过浏览器或者是postman这类的http模拟工具进行,而浏览器和模拟工具在内部会对请求url和参数进行一定处理,例如编码和转义,所以平时对这块关注不多,而当我们在server端自我构建http请求进行远程调用时这类问题就需要我们特别注意,稍有不慎就会掉入坑中,还有一点感悟在使用一个工具类时应当先大致阅读一下工具类提供了哪些方法,做到心中有数使用时就会少走很多弯路。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。