feign 調(diào)用第三方服務(wù)中部分特殊符號(hào)未轉(zhuǎn)義問題
調(diào)用第三方部分特殊符號(hào)未轉(zhuǎn)義
開發(fā)過(guò)程中,發(fā)現(xiàn)+(加號(hào))這個(gè)符號(hào)沒有轉(zhuǎn)義,導(dǎo)致再調(diào)用服務(wù)的時(shí)候把加號(hào)轉(zhuǎn)義成空格了。導(dǎo)致后臺(tái)獲取到的數(shù)據(jù)會(huì)不正確。
1. 問題發(fā)現(xiàn)過(guò)程
feign 解析參數(shù)的時(shí)候,使用的標(biāo)準(zhǔn)是 RFC 3986,這個(gè)標(biāo)準(zhǔn)的加號(hào)是不需要被轉(zhuǎn)義的。其具體的實(shí)現(xiàn)是 feign.template.UriUtils#encodeReserved(String value, String reserved, Charset charset)
2. 解決辦法
feign 調(diào)用過(guò)程
1. feign核心先將(定義好的feign接口)接口中的參數(shù)解析出來(lái)
2. 對(duì)接實(shí)際參數(shù)和接口參數(shù)(入?yún)⒄{(diào)用的參數(shù))
3. 對(duì)入?yún)⒌膮?shù)進(jìn)行編碼(UriUtils#encodeReserved)(問題出在這里)
4. 調(diào)用注冊(cè)的 RequestInterceptor(自定義)
5. Encoder 實(shí)現(xiàn)類,這里是body里面的內(nèi)容才會(huì)有調(diào)用(自定義)
6. 具體的http網(wǎng)絡(luò)請(qǐng)求邏輯
依據(jù)上面的過(guò)程,我們可以實(shí)現(xiàn)一個(gè) RequestInterceptor 攔截器,在這里對(duì)參數(shù)再次進(jìn)行轉(zhuǎn)義即可。
public void apply(RequestTemplate template) { ? ? Map<String, Collection<String>> _queries = template.queries(); ? ? if (!_queries.isEmpty()) { ? ? ? ? //由于在最新的 ?RFC 3986 ?規(guī)范,+號(hào)是不需要編碼的,因此spring 實(shí)現(xiàn)的是這個(gè)規(guī)范,這里就需要參數(shù)中進(jìn)行編碼先,兼容舊規(guī)范。 ? ? ? ? Map<String, Collection<String>> encodeQueries = new HashMap<String, Collection<String>>(_queries.size()); ? ? ? ? Iterator<String> iterator = _queries.keySet().iterator(); ? ? ? ? Collection<String> encodeValues = null; ? ? ? ? while (iterator.hasNext()) { ? ? ? ? ? ? encodeValues = new ArrayList<>(); ? ? ? ? ? ? String key = iterator.next(); ? ? ? ? ? ? Collection<String> values = _queries.get(key); ? ? ? ? ? ? for (String _str : values) { ? ? ? ? ? ? ? ? _str = _str.replaceAll("\\+", "%2B"); ? ? ? ? ? ? ? ? encodeValues.add(_str); ? ? ? ? ? ? } ? ? ? ? ? ? encodeQueries.put(key, encodeValues); ? ? ? ? } ? ? ? ? template.queries(null); ? ? ? ? template.queries(encodeQueries); ? ? } }
上面是代碼片段,詳細(xì)請(qǐng)查看 FeignRequestInterceptor.java
3. 疑問
3.1 是否可以使用 HTTPClient 的實(shí)現(xiàn)就可以解決問題?
也不行,如果不做上面的實(shí)現(xiàn),直接改用HTTPClient實(shí)現(xiàn)的話,也只是在發(fā)送的過(guò)程中起到作用,還是需要在前進(jìn)行處理。
@RequestParams & 符號(hào)未轉(zhuǎn)義
feign-core 版本
<!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-core --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-core</artifactId> <version>10.4.0</version> </dependency>
調(diào)用路徑
源碼分析
1.Template 類
package feign.template; ... public class Template { protected String resolveExpression(Expression expression, Map<String, ?> variables) { String resolved = null; Object value = variables.get(expression.getName()); // 1. 調(diào)用 SimpleExpression 的 expand() 方法 return expression.expand(value, this.encode.isEncodingRequired()); } } public final class Expressions { static class SimpleExpression extends Expression { private final FragmentType type; String encode(Object value) { // 2. 調(diào)用 UriUtils.encodeReserved() 方法,type 參數(shù)是 FragmentType.PATH_SEGMENT return UriUtils.encodeReserved(value.toString(), type, Util.UTF_8); } @Override String expand(Object variable, boolean encode) { StringBuilder expanded = new StringBuilder(); expanded.append((encode) ? encode(variable) : variable); String result = expanded.toString(); return result; } } } public class UriUtils { public static String encodeReserved(String value, FragmentType type, Charset charset) { return encodeChunk(value, type, charset); } private static String encodeChunk(String value, FragmentType type, Charset charset) { byte[] data = value.getBytes(charset); ByteArrayOutputStream encoded = new ByteArrayOutputStream(); for (byte b : data) { if (type.isAllowed(b)) { // 3.1 如果不需要轉(zhuǎn)義,則不進(jìn)行轉(zhuǎn)義操作 encoded.write(b); } else { /* percent encode the byte */ // 3.2 否則,進(jìn)行編碼 pctEncode(b, encoded); } } return new String(encoded.toByteArray()); } enum FragmentType { URI { @Override boolean isAllowed(int c) { return isUnreserved(c); } }, PATH_SEGMENT { @Override boolean isAllowed(int c) { return this.isPchar(c) || (c == '/'); } } abstract boolean isAllowed(int c); protected boolean isAlpha(int c) { return (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'); } protected boolean isDigit(int c) { return (c >= '0' && c <= '9'); } protected boolean isSubDelimiter(int c) { return (c == '!') || (c == '$') || (c == '&') || (c == '\'') || (c == '(') || (c == ')') || (c == '*') || (c == '+') || (c == ',') || (c == ';') || (c == '='); } protected boolean isUnreserved(int c) { return this.isAlpha(c) || this.isDigit(c) || c == '-' || c == '.' || c == '_' || c == '~'; } protected boolean isPchar(int c) { return this.isUnreserved(c) || this.isSubDelimiter(c) || c == ':' || c == '@'; } } }
從源碼上可以看出,& 字符屬于 isSubDelimiter(),所以不會(huì)被轉(zhuǎn)義。
測(cè)試
package feign.template; import feign.Util; public class UriUtilsDemo { ? ? public static void main(String[] args) { ? ? ? ? String str = "aa&aa"; ? ? ? ? // 輸出:aa&aa ? ? ? ? System.out.println(UriUtils.encodeReserved(str, UriUtils.FragmentType.PATH_SEGMENT, Util.UTF_8)); ? ? ? ? // 輸出:aa%26aa ? ? ? ? System.out.println(UriUtils.encodeReserved(str, UriUtils.FragmentType.URI, Util.UTF_8)); ? ? } }
解決方案
1、升級(jí) feign-core 版本,feign-core-10.12 已經(jīng)沒有這個(gè)問題。
2、使用 @RequestBody 替換 @RequestParam。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java將Object轉(zhuǎn)換為數(shù)組的代碼
這篇文章主要介紹了Java將Object轉(zhuǎn)換為數(shù)組的情況,今天在使用一個(gè)別人寫的工具類,這個(gè)工具類,主要是判空操作,包括集合、數(shù)組、Map等對(duì)象是否為空的操作,需要的朋友可以參考下2022-09-09Mybatis結(jié)果集映射一對(duì)多簡(jiǎn)單入門教程
本文給大家介紹Mybatis結(jié)果集映射一對(duì)多簡(jiǎn)單入門教程,包括搭建數(shù)據(jù)庫(kù)環(huán)境的過(guò)程,idea搭建maven項(xiàng)目的代碼詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-06-06Java Web開發(fā)之基于Session的購(gòu)物商店實(shí)現(xiàn)方法
這篇文章主要介紹了Java Web開發(fā)之基于Session的購(gòu)物商店實(shí)現(xiàn)方法,涉及Java針對(duì)session的操作及數(shù)據(jù)庫(kù)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10mybatis通過(guò)中間表實(shí)現(xiàn)一對(duì)多查詢功能
這篇文章主要介紹了mybatis通過(guò)中間表實(shí)現(xiàn)一對(duì)多查詢,通過(guò)一個(gè)學(xué)生的id查詢出該學(xué)生所學(xué)的所有科目,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-08-08Java 線程池詳解及創(chuàng)建簡(jiǎn)單實(shí)例
這篇文章主要介紹了Java 線程池詳解及創(chuàng)建簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-02-02Java中的FileInputStream 和 FileOutputStream 介紹_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
FileInputStream 是文件輸入流,它繼承于InputStream。FileOutputStream 是文件輸出流,它繼承于OutputStream。接下來(lái)通過(guò)本文給大家介紹Java中的FileInputStream 和 FileOutputStream,需要的朋友可以參考下2017-05-05