Feign踩坑源碼分析 -- 請求參數分號變逗號

一.案例

  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問題

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章