Feign get接口傳輸對象引發一場追尋

一個報錯引發的追尋之路:

Feign get接口傳輸對象,調用方接口代碼:

@FeignClient(name = "manage")
public interface AccessApiService {

    @RequestMapping(value = "/interface/listWithRules", method = RequestMethod.GET)
    Result<PageQueryResult<InterfaceInfo>> listWithRules(ApplicationSearchParams params);
}

Feign中定義的一個get接口,參數是一個對象,調用後返回一個 「Request method ‘POST’ not supported」的報錯。

追尋開始

斷點打進去看,查看feign.Client.Default內部類的execute方法,最終使用了jdk自帶的HttpURLConnection
最終可以看到這段代碼:

private synchronized OutputStream  getOutputStream0() throws IOException  {
  try {
      if(!this.doOutput)  {
            throw new ProtocolException("cannot  write to a URLConnection if doOutput=false - call setDoOutput(true)");
 } else {
      if(this.method.equals("GET"))  {
           this.method  = "POST";
 }

傳輸對象這種情況,被粗暴的用修改請求method的方式來解決,在http協議裏複雜的內容可以放入body。所以就有了上面莫名其妙的報錯。我們想,如果不修改請求方式,依然使用get,我們就必須要吧對象中的參數一個個拿出來拼接到url裏,試想下這個對象再複雜一些,比如包了其他的對象呢,怎麼拼接?此時我們想一個對象直接轉json丟body傳輸的確是個好辦法,怪不的上面想這麼粗暴的辦法。
但是修改請求方式破壞了服務提供放的接口,看起來不是很優雅。找解決方案的時候說使用httpclient代替jdk自帶方式就可以解決問題了。
具體如下配置:

1,開啓feign的httpclient
feign:
  httpclient:
    enabled: true
2,引入feign-httpclient,注意版本需要和自己依賴的feign版本保持一致
<dependency>
  <groupId>com.netflix.feign</groupId>
  <artifactId>feign-httpclient</artifactId>
  <version>${feign-httpclient}</version>
</dependency>
3,確認引入httpclient包
<dependency>
 <groupId>org.apache.httpcomponents</groupId>
 <artifactId>httpclient</artifactId>
 <version>4.5.3</version>
</dependency>

如此請求服務就通了,可是會發現對象的參數都是空的,還有一步:
服務提供的接口上需要加上@RequestBody來接參數。對,就是@RequestBody!也就是在body裏拿的參數。

Result<PageQueryResult<InterfaceInfo>> listWithRules(@RequestBody ApplicationSearchParams params);

那麼問題來了,一個get請求怎麼來的body呢?
好吧,http協議只是一個協議,理論上只要你想實現無論用什麼請求方式都可以帶body,只是我們規範約定body是post請求專用而已。
那麼httpclient並不會做這個事,還是feign在調用httpclient時,產生了一個get請求並且帶着body。
具體代碼在feign.httpclient.ApacheHttpClient#toHttpUriRequest:

if (request.body() != null) {
    entity = null;
    Object entity;
    if (request.charset() != null) {
        ContentType contentType = this.getContentType(request);
        content = new String(request.body(), request.charset());
        entity = new StringEntity(content, contentType);
    } else {
        entity = new ByteArrayEntity(request.body());
    }

    requestBuilder.setEntity((HttpEntity)entity);
}

至此,似乎我們只能選擇後者來解決了,這種方式似乎也是有點畸形,畢竟都破壞掉http協議規範了。
所以不推薦使用,推薦所有get請求,調用方的接口代碼全部寫成類似如下:

@RequestMapping(value = "/interface/listWithRules", method = RequestMethod.GET)
Result<PageQueryResult<InterfaceInfo>> listWithRules(
        @RequestParam(value = "gatewayName") String gatewayName, @RequestParam(value = "modifiedTime") Integer modifiedTime, @RequestParam(value = "pageIndex") Integer pageIndex, @RequestParam(value = "pageSize") Integer pageSize);

而服務提供方可以使用對象。這裏注意每個參數註解RequestParam必須要帶有value字段,否則會有這個報錯:
「RequestParam.value() was empty on parameter 0」
問題又來了,在spring mvc使用中都是默認參數名字叫什麼這個value就叫什麼的呀,怎麼這裏還必須要自己定義一下呢?
你遇到的問題世界上總有人已經遇到過,我覺得這個解答是最好的,其他你都不用再看了:
https://stackoverflow.com/questions/44313482/fiegn-client-with-spring-boot-requestparam-value-was-empty-on-parameter-0/52099007

再展開一下,feign在解析@RequestParam註解時的代碼在org.springframework.cloud.netflix.feign.annotation.RequestParamParameterProcessor 如下:

public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
   int parameterIndex = context.getParameterIndex();
   Class<?> parameterType = method.getParameterTypes()[parameterIndex];
   MethodMetadata data = context.getMethodMetadata();

   if (Map.class.isAssignableFrom(parameterType)) {
      checkState(data.queryMapIndex() == null, "Query map can only be present once.");
      data.queryMapIndex(parameterIndex);

      return true;
   }

   RequestParam requestParam = ANNOTATION.cast(annotation);
   String name = requestParam.value();
   checkState(emptyToNull(name) != null,
         "RequestParam.value() was empty on parameter %s",
         parameterIndex);
   context.setParameterName(name);

   Collection<String> query = context.setTemplateParameter(name,
         data.template().queries().get(name));
   data.template().query(name, query);
   return true;
}

因爲通過反射又無法拿到方法字段的名字(jdk8以上,可以通過增加編譯參數開啓這個能力),所以feign就放棄了,而spring 爲什麼能做到獲取到方法字段名稱呢?
因爲它有一套使用asm爲基礎解系class文件的能力,就是直接打開class看,那還有什麼查不到的哦。具體類:LocalVariableTableParameterNameDiscoverer 這個類已經關係的asm了。

追尋結束

到這裏一場追尋告一段落,會想一下也很簡單,爲了解決get請求傳輸複雜對象的情況,一個http請求必然會面臨這個問題,但是話說回來,一個get接口規範上不應該會定義什麼複雜對象,而是幾個參數而已,如果需呀很複雜的對象才能完成這個get接口,可能需呀思考這個接口設計的合理性了。

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