關(guān)于feign對(duì)x-www-form-urlencode類型的encode和decode問(wèn)題
對(duì)x-www-form-urlencode類型的encode和decode問(wèn)題
記錄一下開(kāi)發(fā)過(guò)程中遇到的一個(gè)問(wèn)題。
問(wèn)題場(chǎng)景
使用feign調(diào)用另一服務(wù)b時(shí),在feign-client包里跑單測(cè)能調(diào)用成功,在另一項(xiàng)目a引入該feign-client時(shí)使用同樣的參數(shù)調(diào)用失敗。content-type為application/x-www-form-urlencode POST請(qǐng)求
問(wèn)題原因
入?yún)⒅杏幸粋€(gè)String,數(shù)據(jù)是jsonArray,包含","和":",在打印請(qǐng)求的參數(shù)發(fā)現(xiàn),feign-client包里對(duì)參數(shù)encode之后,“,” 和“:"不變,而項(xiàng)目a調(diào)用feign-client對(duì)參數(shù)encode會(huì)把“,” 和“:"encode成%2C和%3A,導(dǎo)致服務(wù)b decode失敗。
后來(lái)debug對(duì)比兩次的不同點(diǎn),發(fā)現(xiàn)關(guān)鍵點(diǎn)在于feign中生成的RequestTemplate不同;一步一步調(diào)試發(fā)現(xiàn),feign-client包中 feign-core版本是10.2.3,項(xiàng)目a的feign-core版本是9.5.1,兩者在生成RequestTemplate中底層對(duì)參數(shù)encode的方法不同,低版本使用的JDK1.8的URLEncode,高版本使用的feign里的UriUtils.encodeReserved。
feign.template.UriUtils.encodeReserved對(duì)參數(shù)編碼時(shí),會(huì)將參數(shù)列表中key-value的value分割為byte數(shù)組,然后依次對(duì)每個(gè)byte進(jìn)行encode,根據(jù)isAllowed方法判斷是否需要encode,pctEncode(b, encoded)方法是真正去encode的地方。下面的代碼可以看到UriUtils.encodeReserved保留了字母數(shù)字逗號(hào)冒號(hào)等字符。而java.net.URLEncode的encode方法不會(huì)保留逗號(hào)冒號(hào)等字符。
private static String encodeChunk(String value, FragmentType type, Charset charset) { byte[] data = value.getBytes(charset); ByteArrayOutputStream encoded = new ByteArrayOutputStream(); // 依次對(duì)每個(gè)byte編碼 for (byte b : data) { // 對(duì)于一些字符不進(jìn)行編碼 if (type.isAllowed(b)) { encoded.write(b); } else { /* percent encode the byte */ pctEncode(b, encoded); } } return new String(encoded.toByteArray()); } boolean isAllowed(int c) { return this.isPchar(c) || (c == '/'); } protected boolean isPchar(int c) { return this.isUnreserved(c) || this.isSubDelimiter(c) || c == ':' || c == '@'; } protected boolean isUnreserved(int c) { return this.isAlpha(c) || this.isDigit(c) || c == '-' || c == '.' || c == '_' || 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 == '='); }
至于為什么服務(wù)b對(duì)URLEncode編碼的參數(shù)解析不了,還待探索,因?yàn)槲覜](méi)看服務(wù)b的decode代碼,不知道服務(wù)b是怎么解析的。
由于服務(wù)b已經(jīng)對(duì)多方提供,不能讓他們適應(yīng)低版本去增加解決方案(事實(shí)上他們也不想動(dòng)代碼),所以只能從發(fā)起方來(lái)解決問(wèn)題。
可能的解決辦法(沒(méi)來(lái)得及嘗試)
1、版本升級(jí),將項(xiàng)目a的feign-core版本升級(jí)到10.2.3,問(wèn)題能解決(已嘗試),但是項(xiàng)目a中已經(jīng)使用低版本的feign與多個(gè)服務(wù)交互,雖然理論上feign會(huì)向下兼容,但是我不敢輕易升級(jí)版本,而且版本號(hào)跨度還挺大,風(fēng)險(xiǎn)太大 = =。
2、將高版本的encode方法提取出來(lái),手動(dòng)配置到feign.encode中
3、加一個(gè)interceptor,將低版本encode的template再特殊decode一次,保持和高版本的一致 (失敗,template屬性是unModifiable)
4、看能否讓項(xiàng)目a調(diào)用b服務(wù)時(shí)使用高版本feign-core ,其他feign仍然使用低版本
5、放棄feign 用 httpclient調(diào)用 。。。。
附:feign的調(diào)用棧
1、 ReflectiveFeign 被反射實(shí)例化
2、SynchronousMethodHandler.invoke
2-1、先實(shí)例化RequestTemplate 此處encode參數(shù)
2-2、executeAndDecode方法,將RequestTemplate build為request,此處會(huì)先執(zhí)行攔截器
2-3、execute 執(zhí)行 訪問(wèn)原程服務(wù)
2-4、將response decode
附上源碼:
// 2、SynchronousMethodHandler.invoke public Object invoke(Object[] argv) throws Throwable { // 2-1、先實(shí)例化RequestTemplate 此處encode參數(shù) RequestTemplate template = buildTemplateFromArgs.create(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } } Object executeAndDecode(RequestTemplate template) throws Throwable { // 2-2、executeAndDecode方法,將RequestTemplate build為request Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) { logger.logRequest(metadata.configKey(), logLevel, request); } Response response; long start = System.nanoTime(); try { // 2-3、execute 執(zhí)行 訪問(wèn)原程服務(wù) response = client.execute(request, options); } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); } throw errorExecuting(request, e); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); boolean shouldClose = true; try { if (logLevel != Logger.Level.NONE) { response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); } if (Response.class == metadata.returnType()) { if (response.body() == null) { return response; } if (response.body().length() == null || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { shouldClose = false; return response; } // Ensure the response body is disconnected byte[] bodyData = Util.toByteArray(response.body().asInputStream()); return response.toBuilder().body(bodyData).build(); } if (response.status() >= 200 && response.status() < 300) { if (void.class == metadata.returnType()) { return null; } else { Object result = decode(response); shouldClose = closeAfterDecode; return result; } } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { Object result = decode(response); shouldClose = closeAfterDecode; return result; } else { throw errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); } throw errorReading(request, response, e); } finally { if (shouldClose) { ensureClosed(response.body()); } } } long elapsedTime(long start) { return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); } Request targetRequest(RequestTemplate template) { // 此處會(huì)先執(zhí)行攔截器 for (RequestInterceptor interceptor : requestInterceptors) { interceptor.apply(template); } return target.apply(template); } Object decode(Response response) throws Throwable { try { // 2-4、將response decode return decoder.decode(response, metadata.returnType()); } catch (FeignException e) { throw e; } catch (RuntimeException e) { throw new DecodeException(response.status(), e.getMessage(), e); } }
feign x-www-form-urlencoded 類型請(qǐng)求
spring發(fā)送 content-type=application/x-www-form-urlencoded 和普通請(qǐng)求不太一樣。
試了好多方式,最后用以下方式成功
@FeignClient( ?? ?name = "ocr-api", ?? ?url = "${orc.idcard-url}", ?? ?fallbackFactory = OcrClientFallbackFactory.class ) public interface OcrClient { ? ?? ?@PostMapping( ?? ??? ?value = "/v1/demo/idcard", ?? ??? ?headers = {"content-type=application/x-www-form-urlencoded"} ?? ?) ?? ?OcrBaseResponse<IdCardResponse> getIdCarInfo(@RequestBody MultiValueMap<String, Object> request); }
Post請(qǐng)求,參數(shù)使用@RequestBody 并且使用 MultiValueMap。
? ? // 測(cè)試代碼 ?? ?@Resource ?? ?private OcrClient ocrClient; ?? ?@GetMapping("getIdCardInfo") ?? ?public Message getIdCardInfo() { ?? ??? ?MultiValueMap<String, Object> req = new LinkedMultiValueMap<>(); ?? ??? ?req.add("request_id", 12343531123L); ?? ??? ?req.add("img_url", "xxx.jpg"); ?? ??? ?req.add("source", -1); ?? ??? ?req.add("out_business_id", 1321434234L); ?? ??? ?OcrBaseResponse<IdCardResponse> idCarInfo = ocrClient.getIdCarInfo(req); ?? ??? ?return Message.success(idCarInfo); ?? ?}
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java 實(shí)現(xiàn)判斷回文數(shù)字的實(shí)例代碼
這篇文章主要介紹了java 實(shí)現(xiàn)判斷回文數(shù)字的實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2017-03-03Java main方法String[]args原理實(shí)例解析
這篇文章主要介紹了Java main方法String[]args原理實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06Spring mvc是如何實(shí)現(xiàn)與數(shù)據(jù)庫(kù)的前后端的連接操作的?
今天給大家?guī)?lái)的是關(guān)于Spring mvc的相關(guān)知識(shí),文章圍繞著Spring mvc是如何實(shí)現(xiàn)與數(shù)據(jù)庫(kù)的前后端的連接操作的展開(kāi),文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06Java利用StringBuffer替換特殊字符的方法實(shí)現(xiàn)
這篇文章主要介紹了Java利用StringBuffer替換特殊字符的方法實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04IntelliJ IDEA右鍵文件夾沒(méi)有Java Class文件的原因及解決方法
這篇文章主要介紹了IntelliJ IDEA右鍵文件夾沒(méi)有Java Class文件的原因及解決方法,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09springboot?serviceImpl初始化注入對(duì)象實(shí)現(xiàn)方式
這篇文章主要介紹了springboot?serviceImpl初始化注入對(duì)象實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05Java時(shí)間類庫(kù)Timer的使用方法與實(shí)例詳解
這篇文章主要介紹了Jave時(shí)間類庫(kù)Timer的使用方法與實(shí)例詳解,需要的朋友可以參考下2020-02-02使用Java實(shí)現(xiàn)一個(gè)能保留計(jì)算過(guò)程的計(jì)算器
計(jì)算器是我們?nèi)粘I钪谐S玫墓ぞ咧?它能夠進(jìn)行基本的數(shù)學(xué)運(yùn)算,如加法、減法、乘法和除法,而在設(shè)計(jì)一個(gè)計(jì)算器時(shí),我們可以通過(guò)使用Java編程語(yǔ)言來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的控制臺(tái)計(jì)算器,并且讓它能夠保留計(jì)算過(guò)程,文中有詳細(xì)的代碼示例,需要的朋友可以參考下2023-11-11SpringBoot使用Validation校驗(yàn)參數(shù)的詳細(xì)過(guò)程
這篇文章主要介紹了SpringBoot使用Validation校驗(yàn)參數(shù),本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09