一.案例
1.1.Post請求:
http://localhost:8250/xx/task/test json格式參數: { "string": "a;b;c;d" }
1.2.controller代碼:
@Autowired DataSourceClientService dataSourceClientService; @RequestMapping("/test") @ResponseBody public void test(@RequestBody String string) { System.out.println(string); String result = dataSourceClientService.test(string); System.out.println(result); }
1.3.feign代碼:
@FeignClient( value = "zz") public interface DataSourceClientService { @RequestMapping(value = "/dataSource/test",method = RequestMethod.POST,produces = "text/plain;charset=UTF-8") String test(@RequestParam("str") String str); }
1.4服務提供方代碼:
@RequestMapping("/test") @ResponseBody public String test(@RequestParam String str) { System.out.println(str); String result = "success;"; return result; }
1.5發起請求後控制檯打印結果:
請求方控制檯: 用戶[null]開始調用web接口:http://localhost:8250/xx/task/test { "string": "a;b;c;d" } 服務提供方控制檯: 用戶[null]開始調用web接口:http://localhost:8247/zz/dataSource/test { "string": "a,b,c,d" }
二.解決辦法
2.1.在請求方對參數進行編碼:
string = UriUtils.encode(string, StandardCharsets.UTF_8);
String test = dataSourceClientService.test(string);
2.2.服務提供方@RequestParam改成@RequestBody;
三.分析
3.1.需求
服務提供方需要的字符串是包含“;”而不是“,”,因爲實際傳遞的是一個JSONObject的字符串,導致JSONUtil.parse轉換成對象失敗,導致業務失敗;
3.2請求方法參數@RequestParam而傳參不加密
3.2.1 請求方源碼探究
通過一步步debug代碼調試,發現調用 dataSourceClientService.test(string) 是jdk動態代理,調用鏈接到
feign.ReflectiveFeign.FeignInvocationHandler#invoke
-->feign.SynchronousMethodHandler#invoke
-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create
-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#resolve
-->feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>)
-->feign.template.QueryTemplate#expand
-->feign.template.QueryTemplate#queryString
static final String COLLECTION_DELIMITER = ";"; private String queryString(String name, String values) { if (this.pure) { return name; } /* covert the comma separated values into a value query string */ List<String> resolved = Arrays.stream(values.split(COLLECTION_DELIMITER)) .filter(Objects::nonNull) .filter(s -> !UNDEF.equalsIgnoreCase(s)) .collect(Collectors.toList()); if (!resolved.isEmpty()) { return this.collectionFormat.join(name, resolved, this.getCharset()).toString(); } /* nothing to return, all values are unresolved */ return null; }
從上面可以看到,一個參數字符串“a;b;c;d”被分割處理成了一個數組,然後重新組合成字符串(Collection)傳遞給服務提供方,服務提供方接收參數是一個string字符串,會用逗號給拼接成字符串。
也就是說,feign預設了使用者如果是用傳參帶了分號過來的,會認爲你傳的是Collection,而不是String。
3.2.1 服務提供方源碼探究
下面是服務提供方接收的參數詳情:
org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument
從上面可以看出,請求解析出來是一個字符串數組,在
org.springframework.core.convert.support.CollectionToStringConverter#convert 中用逗號拼接起來:
3.2.1 調用服務提供方方法前參數編碼:
當沒有編碼前,org.springframework.core.convert.support.GenericConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) 調用鏈進入的是org.springframework.core.convert.support.CollectionToStringConverter#convert 中用逗號拼接起來,因爲參數類型是字符串數組
當編碼後,進入的是org.springframework.core.convert.support.GenericConversionService.NoOpConverter#convert,直接返回字符串:
參數值的獲取在org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName 中
參數的解碼方法在org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset)中進入
在org.apache.tomcat.util.http.Parameters#urlDecode進行解碼
前面的是參數進行編碼後解碼,因爲編碼成一個字符串,所以解碼的也是字符串,當沒有進行編碼,傳遞的會認爲是一個collection,所以會遍歷解碼:
然後加入到數組中:
3.3請求方法參數改成@RequestBody
3.3.1服務提供方
當請求方法是@RequestBody時,獲取參數在org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument:
直接從httprequest請求中獲取body參數值:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(org.springframework.http.HttpInputMessage, org.springframework.core.MethodParameter, java.lang.reflect.Type)
3.3.2請求方
參數直接封裝到body中:
feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create
--> feign.ReflectiveFeign.BuildEncodedTemplateFromArgs#resolve
對比3.2.1流程會發現,body不會進行字符串的切割,當然拉,也跟參數的請求類型不一致有關,一個是query,一個是body,下面是3.2.1流程
-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create
-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#resolve
-->feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>)
-->feign.template.QueryTemplate#expand
-->feign.template.QueryTemplate#queryString
四.擴展
4.1.get請求
get請求的參數類型是query,所以走的是3.2.1流程,所以string的傳參中包含“;”也會被轉換成“,”;
@FeignClient( value = "zz") public interface DataSourceClientService { @RequestMapping(value = "/dataSource/testGet",method = RequestMethod.GET,produces = "text/plain;charset=UTF-8") String testGet(@RequestParam("str") String str); } @RequestMapping(value = "/testGet") @ResponseBody public String testGet(String str) { System.out.println(str); String result = "success"; return result; } 服務提供方控制檯輸出: 用戶[null]開始調用web接口:http://localhost:8247/zz/dataSource/testGet a,b,c,d
ps:當請求參數字符串存在特殊符號比如“+”時,會被轉義爲空格
http://localhost:8250/xx/task/test2?string=a+b 請求方控制檯輸出: 用戶[null]開始調用web接口:用戶[null]開始調用web接口:http://localhost:8250/xx/task/test2 a b
通過debug調試發現當發起請求時在org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest中獲取參數:
繼續調用鏈:
-->org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument
-->org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument
-->org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName
-->org.springframework.web.context.request.ServletWebRequest#getParameterValues
-->org.apache.catalina.connector.RequestFacade#getParameterValues
-->org.apache.catalina.connector.Request#getParameterValues
-->org.apache.coyote.Request#getParameters
從上面可以得知參數在parameters中,並且特殊符號已經被處理過了,那接下來找下什麼時候放進來和怎麼被處理的;在org.apache.catalina.connector.Request中看見了parseParameters方法,可以看出是對參數的解析方法,在裏面打下斷點重新跑下:
從下面可以看出這裏是真正進行參數解析處理的:
org.apache.tomcat.util.http.Parameters#handleQueryParameters:
-->org.apache.tomcat.util.http.Parameters#processParameters(org.apache.tomcat.util.buf.MessageBytes, java.nio.charset.Charset)
-->org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset)
-->org.apache.tomcat.util.http.Parameters#urlDecode
從上面可以看到,是在進行解碼時將“+”去除掉的;
繼續debug發現在下面的代碼中,可以清晰的看到對“+”替換成空格;
-->org.apache.tomcat.util.buf.UDecoder#convert(org.apache.tomcat.util.buf.ByteChunk, boolean)
解決辦法爲:
1)對參數“a+b”進行編碼;
2)改成post請求;
4.2.header參數
在網上看見一篇文章,在這裏收藏下《FeignClient傳入的header中帶逗號引發的401問題》