關(guān)于遠(yuǎn)程調(diào)用RestTemplate的使用避坑指南
一、前言介紹
RestTemplate是Spring中用于遠(yuǎn)程接口調(diào)用的工具類,它是Apache的HttpClient的模板封裝,使用起來非常方便,本文將講述這兩天自己在使用RestTemplate過程中遇到的問題,當(dāng)然這些問題也是由于自己對(duì)RestTemplate工具類了解不夠全面不夠透徹造成的,希望自己遇到的這些問題能為大家提前避雷或是遇到類似問題時(shí)的一個(gè)解決參考。
二、 問題記錄
1. 慎!【url參數(shù)中有json字符串】
在使用RestTemplate進(jìn)行遠(yuǎn)程接口調(diào)用時(shí),如果url拼接參數(shù)中json字符串時(shí)一定要小心,使用場景如下:利用restTemplate調(diào)用user的查詢信息接口,url中的一個(gè)參數(shù)user為json字符串格式{\"user\":\"xiaoming,\"age\":"12"}
// JSON參數(shù)
Map<String, String> paramMap = new HashMap<>(8);
paramMap.put("name","xiaoming");
paramMap.put("age","12");
String paramJsonStr = JSONObject.toJSONString(paramMap);
// 實(shí)際參數(shù) 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();
// 調(diào)用出錯(cuò)
Object execute = restTemplate.execute(url, HttpMethod.GET, null, null);
此時(shí)當(dāng)我們運(yùn)行程序時(shí)會(huì)拋出以下錯(cuò)誤:

錯(cuò)誤意思大概是沒有足夠可用的變量值來填充擴(kuò)展 'name',這是什么鬼意思,別著急讓我們跟跟代碼看看異常拋出的位置,最終定位如下,在創(chuàng)建URI過程中調(diào)用了 UriComponents.expandUriComponent()方法拋出異常:

這段代碼的作用其實(shí)就是通過NAMES_PATERN規(guī)則匹配到相應(yīng)字符串然后利用 uriVariables.getValue(varibaleName)進(jìn)行替換,再看看NAMES_PATERN的值就是用來匹配{}中的字符串內(nèi)容的
private static final Pattern NAMES_PATTERN = Pattern.compile(\\{([^/]+?)\\});
問題分析到這兒相信大家應(yīng)該也明白了RestTemplate在創(chuàng)建URI時(shí)會(huì)進(jìn)行{param}占位替換,這個(gè)規(guī)則在文本輸出時(shí)應(yīng)用比較多,如日志打印和控制臺(tái)打印中常有使用:
String value = "test";
logger.info("占位參數(shù){}",value);
解決辦法:
找到原因了,那么我們應(yīng)當(dāng)如何解決呢,既然RestTemplate在處理url時(shí)會(huì)進(jìn)行{}變量替換,那它理應(yīng)提供相應(yīng)的接口調(diào)用,查看RestTemplate源碼它提供了多個(gè)exchange重載方法,其中多個(gè)方法都有uriVariables參數(shù),如下所示:
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參數(shù)中有經(jīng)過URLEncode的字符串】
其實(shí)在遇到第一個(gè)坑時(shí),我并沒有采用上面給出的解決方式,而是想著將Json字符串經(jīng)過URLEncode編碼后在拼接到url后面,不就沒有{}符號(hào)了,不就可以完美解決問題了,心里想著就美滋滋,那讓我們來試一把吧:
// JSON參數(shù)
Map<String, String> paramMap = new HashMap<>(8);
paramMap.put("name","xiaoming");
paramMap.put("age","12");
String paramJsonStr = JSONObject.toJSONString(paramMap);
// 實(shí)際參數(shù) 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字符串經(jīng)過編碼后已經(jīng)沒有{}符號(hào)了,也能夠成功調(diào)用接口,但是接口提供方無情的返回了錯(cuò)誤:參數(shù)無法反序列化。聽這口氣肯定也是這json串的原因,趕緊用Postman試一試:

神奇的一幕出現(xiàn)了,居然成功了! Postman方式調(diào)用和RestTemplate調(diào)用有什么不一樣,為什么postman行,restTemplate不行?趕緊查看服務(wù)器日志看看兩種方式接收到的參數(shù)有和不一樣:
1. Postman方式服務(wù)器接收到的url:
"http://localhost:8080/api/user?user=%7B%22name%22%3A%22xiaoming%22%2C%22age%22%3A%2212%22%7D&country=china"
2. RestTemplate方式服務(wù)器接收到的url
"http://localhost:8080/api/user?user=%257B%2522name%2522%3A%2522xiaoming%2522%2C%2522age%2522%253A%252212%2522%257D&country=china"
restTemplate居然在每個(gè)百分號(hào)%后面都擅自加了25這個(gè)數(shù)字,難怪服務(wù)端沒法解析,它為什么要這么做?難道是restTemplate的url處理bug?讓我們跟一跟代碼看個(gè)究竟:

詳細(xì)調(diào)用層次就不貼了,簡單來說就是RestTemplate在處理url時(shí)會(huì)對(duì)url參數(shù)進(jìn)行再編碼,也就是會(huì)對(duì)url中的特殊字符進(jìn)行轉(zhuǎn)義,如%號(hào)會(huì)被轉(zhuǎn)義為%25,所以傳給服務(wù)端的url就被改變了,具體url特殊字符轉(zhuǎn)義知識(shí)請(qǐng)查看這篇文章
既然知道了原因,那么我們應(yīng)該如何解決呢?
解決方案:

RestTemplate中的URI對(duì)象是通過UriTemplateHandler生成的,所以我們只需要利用java.net包中的URI自己構(gòu)建URI對(duì)象傳給RestTemplate即可,這樣url中的特殊字符就不會(huì)被轉(zhuǎn)義了:

3. 慎!【url參數(shù)中存在特殊字符】 --- 針對(duì)HttpClient
前面兩個(gè)坑然我對(duì)RestTemplate有點(diǎn)望而生畏了,既然RestTemplate這么多坑,那好咋們換回老家伙apache家族的HttpClient,本以為可以一切順利,沒成想一坑接著一坑啊!!!
先看看調(diào)用場景:按照出生時(shí)間去查詢用戶信息

調(diào)用請(qǐng)求都還沒發(fā)出就無情報(bào)錯(cuò)了:

通過異常信息可以容易知道在創(chuàng)建URL對(duì)象時(shí)url參數(shù)索引位置50處有非法字符,而這個(gè)字符剛好就是時(shí)間參數(shù)中的空格!,跟蹤代碼異常發(fā)生位置發(fā)現(xiàn)下面一段注釋,大意就是不允許url中有特殊字符存在,看了本文第二個(gè)坑的應(yīng)該已經(jīng)明白u(yù)rl中特殊字符為什么需要進(jìn)行轉(zhuǎn)義了,這里就不詳細(xì)敘述,至此解決方案就呼之欲出了。

解決方案:
需要對(duì)參數(shù)中的特殊字符進(jìn)行轉(zhuǎn)義:
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可以對(duì)路徑、參數(shù)、片段進(jìn)行處理,提供了 path,parameter,fragment三個(gè)部分的Escape實(shí)例
分別調(diào)用UrlEscapers類的以下方法獲取:
urlFormParameterEscaper()urlPathSegmentEscaper()urlFragmentEscaper()

總結(jié)
本次三個(gè)案例本人覺得還是具有典型性,由于平時(shí)發(fā)起請(qǐng)求大多通過瀏覽器或者是postman這類的http模擬工具進(jìn)行,而瀏覽器和模擬工具在內(nèi)部會(huì)對(duì)請(qǐng)求url和參數(shù)進(jìn)行一定處理,例如編碼和轉(zhuǎn)義,所以平時(shí)對(duì)這塊關(guān)注不多,而當(dāng)我們?cè)趕erver端自我構(gòu)建http請(qǐng)求進(jìn)行遠(yuǎn)程調(diào)用時(shí)這類問題就需要我們特別注意,稍有不慎就會(huì)掉入坑中,還有一點(diǎn)感悟在使用一個(gè)工具類時(shí)應(yīng)當(dāng)先大致閱讀一下工具類提供了哪些方法,做到心中有數(shù)使用時(shí)就會(huì)少走很多彎路。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
ElasticSearch不停機(jī)重建索引延伸思考及優(yōu)化詳解
這篇文章主要為大家介紹了ElasticSearch不停機(jī)重建索引延伸思考及優(yōu)化詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02
springboot2.5.2與 flowable6.6.0整合流程引擎應(yīng)用分析
這篇文章主要介紹了springboot2.5.2與 flowable6.6.0整合流程引擎應(yīng)用分析,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-07-07
一篇文章大家徹底學(xué)會(huì)Java之格式化輸出
Java提供了多種格式化輸出的方式,包括String.format()、System.out.printf()和Formatter類,這些方法支持?jǐn)?shù)字、日期、字符串等多種格式化操作,這篇文章主要介紹了Java格式化輸出的相關(guān)資料,需要的朋友可以參考下2025-04-04
詳解如何在Java中使用阿里云對(duì)象存儲(chǔ)OSS
Java是世界上最流行的編程語言之一,擁有著廣泛的應(yīng)用場景和強(qiáng)大的生態(tài)系統(tǒng),阿里云對(duì)象存儲(chǔ) OSS 是一種企業(yè)級(jí)的云存儲(chǔ)服務(wù),本文將介紹如何在 Java 中使用阿里云對(duì)象存儲(chǔ) OSS,并寫一點(diǎn)相應(yīng)的代碼示例供大家參考2023-06-06

