47

190306-SpringCloud之Feign请求参数包装异常问题定位

 1年前 阅读数 48
以下为 快照 页面,建议前往来源网站查看,会有更好的阅读体验。
原文链接: http://spring.hhui.top/spring-blog/2019/03/06/190306-SpringCloud之Feign请求参数包装异常问题定位/?amp%3Butm_medium=referral

通过Feign包装rpc的调用姿势,在使用的版本中发现一个奇怪的bug,大部分场景下请求正常,少数情况下请求返回400,记录下原因

场景复现

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集合,直接被吃掉了

3EZfqaf.jpg!web

再对应到我们的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,空集合,则不会将参数拼接到最终的请求参数中,也就导致最终发起请求时,少了一个参数

a6vaumJ.jpg!web

问题清晰之后,然后就可以确认下是bug还是就是这么设计的了,最简单的办法就是看最新的代码有没有改掉了,从git上,目前已经更新到10.x;10.x与9.x的差别挺大,底层很多东西重写了,然而官方的 Spring-Cloud-openfeing 并没有升级到最新,so,只能取看9.7.0版本的实现了,和9.5.2并没有太大的区别;

so,站在feign开发者角度出发,这么设计的理由可能有以下几点

require=False

3. 小结

最后小结一下,使用feign作为SpringCloud的rpc封装工具时,请注意,

  • 如果api的请求参数允许为null,请在注解中显示声明;
  • 此外请求方传入的null、空集合最终不会拼装的请求参数中,即对于接受者而言,就像没有这个参数一样,对于出现400错误的场景,可以考虑下是否是这种问题导致的
  • 对于复杂的请求参数,推荐使用DTO来替代多参数的类型(因为这样接口的复用性是最佳的,如新增和修改条件时,往往不需要新增api)

II. 其他

0. 项目

1. 一灰灰Blog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

  • 微博地址:小灰灰Blog
  • QQ: 一灰灰/3302797840

3. 扫描关注

一灰灰blog

RfYn2mj.png!web

知识星球

fQrAZjq.png!web

打赏 如果觉得我的文章对您有帮助,请随意打赏。


猜你喜欢

  • 30
    • 博客园 www.cnblogs.com 2年前
    • 快照

    springmvc 请求参数解析细节

    springmvc 的请求流程,相信大家已经很熟悉了,不熟悉的同学可以参考下资料! 有了整体流程的概念,是否对其中的实现细节就很清楚呢?我觉得不一定,比如:单是参数解析这块,就是个大学问呢? 首先,我们从最靠近请求末...

  • 32

    内网通过K8S搭建多个分支测试环境,可是如果外网需要访问而且域名都是一致的情况下,这个时候变得麻烦了。如何通过不同的请求参数访问不同的后端环境呢,答案是可以的,通过lua可以达到。入口:http://fengwan.blog.51cto.com/?envs=branches#branches环境http://fe...

  • 22
    • www.throwable.club 1年前
    • 快照

    SpringMVC请求参数接收总结(一)

    在日常使用 SpringMVC 进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结。 SpringMVC 中处理控制器参数的接口是 HandlerMethodArgumentResolver ,此接口有众多子类...

  • 9

    If you need performance and good productivity, you will love Gin. 这是 Gin 源码学习的第一篇,为什么是 Gin 呢? 正如 Gin 官方文档中所说,Gin 是一个注重性能和生产的 web 框架,并且号称其性能要比 httpr...

  • 0

    使用 POST 方式请求 JSON 数据到服务器 WebAPI 接口时需要将 JSON 格式封装成数据模型接收参数。即使参数较少,每个接口仍然需要单独创建模型接收。下面方法实现了将 JSON 参数中的字段作为接口参数接收。实现的并不完美,现在只支持 JSON...

  • 84

    当前后端分离时,权限问题的处理也和我们传统的处理方式有一点差异。笔者前几天刚好在负责一个项目的权限管理模块,现在权限管理模块已经做完了,我想通过5-6篇文章,来介绍一下项目中遇到的问题以及我的解决方案...

  • 14
    • 微信 mp.weixin.qq.com 1年前
    • 快照

    SpringCloud Gateway 测试问题解决

  • 1

    SpringCloud 之 Ribbon/Feign/Hystrix 的超时、重试问题总结 Hi,我是空夜,又是一周不见!今天来讲讲 ribbon 和 feign 中超时时间应该如何配置。Spring Cloud 中,一般会用 feign 或...

  • 17
    • studygolang.com 10个月前
    • 快照

    Golang参数传递问题

    首先说结论:在Go语言里,所有的参数传递都是值传递(传值),都是一个副本,一个拷贝,因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这...

  • 24
    • blog.codingplayboy.com 2年前
    • 快照

    React Native babel编译异常问题解决

    React Native babel编译异常问题解决

关于极客头条


聚合每日国内外有价值,有趣的链接。

AD