feign 調(diào)用第三方服務中部分特殊符號未轉(zhuǎn)義問題
調(diào)用第三方部分特殊符號未轉(zhuǎn)義
開發(fā)過程中,發(fā)現(xiàn)+(加號)這個符號沒有轉(zhuǎn)義,導致再調(diào)用服務的時候把加號轉(zhuǎn)義成空格了。導致后臺獲取到的數(shù)據(jù)會不正確。
1. 問題發(fā)現(xiàn)過程
feign 解析參數(shù)的時候,使用的標準是 RFC 3986,這個標準的加號是不需要被轉(zhuǎn)義的。其具體的實現(xiàn)是 feign.template.UriUtils#encodeReserved(String value, String reserved, Charset charset)
2. 解決辦法
feign 調(diào)用過程
1. feign核心先將(定義好的feign接口)接口中的參數(shù)解析出來
2. 對接實際參數(shù)和接口參數(shù)(入?yún)⒄{(diào)用的參數(shù))
3. 對入?yún)⒌膮?shù)進行編碼(UriUtils#encodeReserved)(問題出在這里)
4. 調(diào)用注冊的 RequestInterceptor(自定義)
5. Encoder 實現(xiàn)類,這里是body里面的內(nèi)容才會有調(diào)用(自定義)
6. 具體的http網(wǎng)絡請求邏輯
依據(jù)上面的過程,我們可以實現(xiàn)一個 RequestInterceptor 攔截器,在這里對參數(shù)再次進行轉(zhuǎn)義即可。
public void apply(RequestTemplate template) { ? ? Map<String, Collection<String>> _queries = template.queries(); ? ? if (!_queries.isEmpty()) { ? ? ? ? //由于在最新的 ?RFC 3986 ?規(guī)范,+號是不需要編碼的,因此spring 實現(xiàn)的是這個規(guī)范,這里就需要參數(shù)中進行編碼先,兼容舊規(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); ? ? } }
上面是代碼片段,詳細請查看 FeignRequestInterceptor.java
3. 疑問
3.1 是否可以使用 HTTPClient 的實現(xiàn)就可以解決問題?
也不行,如果不做上面的實現(xiàn),直接改用HTTPClient實現(xiàn)的話,也只是在發(fā)送的過程中起到作用,還是需要在前進行處理。
@RequestParams & 符號未轉(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)義,則不進行轉(zhuǎn)義操作 encoded.write(b); } else { /* percent encode the byte */ // 3.2 否則,進行編碼 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(),所以不會被轉(zhuǎn)義。
測試
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、升級 feign-core 版本,feign-core-10.12 已經(jīng)沒有這個問題。
2、使用 @RequestBody 替換 @RequestParam。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Java將Object轉(zhuǎn)換為數(shù)組的代碼
這篇文章主要介紹了Java將Object轉(zhuǎn)換為數(shù)組的情況,今天在使用一個別人寫的工具類,這個工具類,主要是判空操作,包括集合、數(shù)組、Map等對象是否為空的操作,需要的朋友可以參考下2022-09-09Java Web開發(fā)之基于Session的購物商店實現(xiàn)方法
這篇文章主要介紹了Java Web開發(fā)之基于Session的購物商店實現(xiàn)方法,涉及Java針對session的操作及數(shù)據(jù)庫操作技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-10-10Java中的FileInputStream 和 FileOutputStream 介紹_動力節(jié)點Java學院整理
FileInputStream 是文件輸入流,它繼承于InputStream。FileOutputStream 是文件輸出流,它繼承于OutputStream。接下來通過本文給大家介紹Java中的FileInputStream 和 FileOutputStream,需要的朋友可以參考下2017-05-05