内容简介:通过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 绝对定位释义
- 如何定位渲染耗时瓶颈
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Learn Python the Hard Way
Zed Shaw / Example Product Manufacturer / 2011
This is a very beginner book for people who want to learn to code. If you can already code then the book will probably drive you insane. It's intended for people who have no coding chops to build up t......一起来看看 《Learn Python the Hard Way》 这本书的介绍吧!