内容简介:通过Feign包装rpc的调用姿势,在使用的版本中发现一个奇怪的bug,大部分场景下请求正常,少数情况下请求返回400,记录下原因Spring版本如Feign版本
通过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集合,直接被吃掉了
再对应到我们的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,空集合,则不会将参数拼接到最终的请求参数中,也就导致最终发起请求时,少了一个参数
问题清晰之后,然后就可以确认下是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. 项目
- 工程: spring-boot-demo
1. 一灰灰Blog
- 一灰灰Blog个人博客 https://blog.hhui.top
- 一灰灰Blog-Spring专题博客 http://spring.hhui.top
一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛
2. 声明
尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
- 微博地址:小灰灰Blog
- QQ: 一灰灰/3302797840
3. 扫描关注
一灰灰blog
知识星球
打赏 如果觉得我的文章对您有帮助,请随意打赏。
以上所述就是小编给大家介绍的《190306-SpringCloud之Feign请求参数包装异常问题定位》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 认识绝对定位,相对定位
- 移动端页面头部固定定位的绝对定位实现
- webgl(three.js)实现室内定位,楼宇bim、实时定位三维可视化解决方案——第五课
- IP 地址怎么定位?
- # CSS 绝对定位释义
- 如何定位渲染耗时瓶颈
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Design in a Nutshell
Jennifer Niederst / O'Reilly Media, Inc. / 2006-02-21 / USD 34.99
Are you still designing web sites like it's 1999? If so, you're in for a surprise. Since the last edition of this book appeared five years ago, there has been a major climate change with regard to web......一起来看看 《Web Design in a Nutshell》 这本书的介绍吧!