一、前言介紹
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請求進行遠程調用時這類問題就需要我們特別注意,稍有不慎就會掉入坑中,還有一點感悟在使用一個工具類時應當先大致閱讀一下工具類提供了哪些方法,做到心中有數使用時就會少走很多彎路。
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/u012417552/article/details/102875142