天天看點

SpringCloud之Feign請求參數包裝異常問題定位

通過Feign包裝rpc的調用姿勢,在使用的版本中發現一個奇怪的bug,大部分場景下請求正常,少數情況下請求傳回400,記錄下原因

SpringCloud之Feign請求參數包裝異常問題定位

場景複現

1. 環境相關版本

Spring版本如

<spring.boot.version>2.0.1.RELEASE</spring.boot.version>
<spring.cloud.version>Finchley.RELEASE</spring.cloud.version>      

Feign版本

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>      

對應的feign-core版本為

<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>9.5.1</version>      

2. 服務接口

接口形如

@RequestMapping(value = "getMarketDailySummary")
BaseRsp<MarketDailySummaryDTO> getMarketDailySummary(@RequestParam("datetime") Long datetime,
         @RequestParam(value = "coinIds") List<Integer> coinIds,
         @RequestParam(value = "pairIds") List<Integer> pairIds);      

使用時報400的case

marketDailyReportService.getMarketDailySummary(1551836411000L, Arrays.asList(1, 2, 3, 10), Arrays.asList());      

簡單來說,接口參數為集合的情況下,如果傳一個空集合,那麼這就會出現400的錯誤

通過在提供服務的應用中,寫一個fitler攔截請求,列印出請求參數

@Component
@WebFilter(value = "/**")
public class ReqFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
            throws IOException, ServletException {
        try {
            System.out.println(servletRequest.getParameterMap());
        } finally {
            filterChain.doFilter(servletRequest, servletResponse);
        }

    }

    @Override
    public void destroy() {

    }
}      

然後發起rpc調用前面的測試用例,通過斷點檢視請求參數,确實隻有兩個參數,而我們傳入空pairIds集合,直接被吃掉了

SpringCloud之Feign請求參數包裝異常問題定位

再對應到我們的api聲明方式,要求三個參數,是以問題就很清晰了,解決辦法就是在api中參數的必填設定為false即可

@RequestMapping(value = "getMarketDailySummary")
BaseRsp<MarketDailySummaryDTO> getMarketDailySummary(@RequestParam("datetime") Long datetime,
       @RequestParam(value = "coinIds", required = false) List<Integer> coinIds,
       @RequestParam(value = "pairIds", required = false) List<Integer> pairIds);      

上面隻是表層的解決了問題,接下來就需要确定,為什麼請求參數會被吃掉,通過淺顯的推測,多半原因在feign的請求參數封裝上了

2. 問題定位

對于容易複現的問題,最佳的定位方法就是debug了,直接單步進去,找到對應的請求參數封裝邏輯,

第一步定位到​

​RequestTemplate​

​的建立

// feign.SynchronousMethodHandler#invoke
 @Override
public Object invoke(Object[] argv) throws Throwable {
  // 下面這一行為目标邏輯,建立請求模闆類,請求參數封裝肯定是在裡面了
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      return executeAndDecode(template);
    } catch (RetryableException e) {
      retryer.continueOrPropagate(e);
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
      }
      continue;
    }
  }
}      

接下來深入進去之後,參數解析的位置

// feign.ReflectiveFeign.BuildTemplateByResolvingArgs#resolve
protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable,
                                  Map<String, Object> variables) {
    // Resolving which variable names are already encoded using their indices
    Map<String, Boolean> variableToEncoded = new LinkedHashMap<String, Boolean>();
    for (Entry<Integer, Boolean> entry : metadata.indexToEncoded().entrySet()) {
      Collection<String> names = metadata.indexToName().get(entry.getKey());
      for (String name : names) {
        variableToEncoded.put(name, entry.getValue());
      }
    }

    // 核心邏輯了,使用請求參數來替換模闆中的占位
    return mutable.resolve(variables, variableToEncoded);
  }
}      

再進去一步就到了根源點

// feign.RequestTemplate#replaceQueryValues(java.util.Map<java.lang.String,?>, java.util.Map<java.lang.String,java.lang.Boolean>)
void replaceQueryValues(Map<String, ?> unencoded, Map<String, Boolean> alreadyEncoded) {
  Iterator<Entry<String, Collection<String>>> iterator = queries.entrySet().iterator();
  while (iterator.hasNext()) {
    Entry<String, Collection<String>> entry = iterator.next();
    if (entry.getValue() == null) {
      continue;
    }
    Collection<String> values = new ArrayList<String>();
    for (String value : entry.getValue()) {
      if (value.indexOf('{') == 0 && value.indexOf('}') == value.length() - 1) {
        Object variableValue = unencoded.get(value.substring(1, value.length() - 1));
        // only add non-null expressions
        if (variableValue == null) {
          // 如果請求參數為null,也不會憑借到url參數中
          continue;
        }
        if (variableValue instanceof Iterable) {
          // 将目标集中在這裡,如果請求參數時空集合,下面的for循環不會走到,是以也就不會拼接在url參數中
          for (Object val : Iterable.class.cast(variableValue)) {
            String encodedValue = encodeValueIfNotEncoded(entry.getKey(), val, alreadyEncoded);
            values.add(encodedValue);
          }
        } else {
          String encodedValue = encodeValueIfNotEncoded(entry.getKey(), variableValue, alreadyEncoded);
          values.add(encodedValue);
        }
      } else {
        values.add(value);
      }
    }
    if (values.isEmpty()) {
      iterator.remove();
    } else {
      entry.setValue(values);
    }
  }
}      

下圖是我們最終定位的一個截圖,從代碼實作來看,feign的設計理念是,如果請求參數為null,空集合,則不會将參數拼接到最終的請求參數中,也就導緻最終發起請求時,少了一個參數

SpringCloud之Feign請求參數包裝異常問題定位

問題清晰之後,然後就可以确認下是bug還是就是這麼設計的了,最簡單的辦法就是看最新的代碼有沒有改掉了,從git上,目前已經更新到10.x;10.x與9.x的差别挺大,底層很多東西重寫了,然而官方的​

​Spring-Cloud-openfeing​

​并沒有更新到最新,so,隻能取看9.7.0版本的實作了,和9.5.2并沒有太大的差別;

so,站在feign開發者角度出發,這麼設計的理由可能有以下幾點

  • 既然允許傳入空集合、null參數,那麼在api的聲明時,就有必要加上 ​

    ​require=False​

  • 對于這種無效的請求參數,也沒有太大的必要傳過去(雖然從使用者角度來說,你就應該老老實實的把我調用的參數都丢過去)

3. 小結

最後小結一下,使用feign作為SpringCloud的rpc封裝工具時,請注意,

  • 如果api的請求參數允許為null,請在注解中顯示聲明;
  • 此外請求方傳入的null、空集合最終不會拼裝的請求參數中,即對于接受者而言,就像沒有這個參數一樣,對于出現400錯誤的場景,可以考慮下是否是這種問題導緻的
  • 對于複雜的請求參數,推薦使用DTO來替代多參數的類型(因為這樣接口的複用性是最佳的,如新增和修改條件時,往往不需要新增api)

II. 其他

0. 項目

  • 工程:https://github.com/liuyueyi/spring-boot-demo

1. 一灰灰Blog

  • 一灰灰Blog個人部落格 https://blog.hhui.top
  • 一灰灰Blog-Spring專題部落格 http://spring.hhui.top

一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 聲明

盡信書則不如,以上内容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

  • 微網誌位址: 小灰灰Blog

3. 掃描關注

一灰灰blog