一 起因
之前写一个小 demo,惯例使用自己归纳起来的方式集成 Swagger 来做 api 调试,然后启动时报了个错:
org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:181) ~[spring-context-5.3.14.jar:5.3.14]
at org.springframework.context.support.DefaultLifecycleProcessor.access$200(DefaultLifecycleProcessor.java:54) ~[spring-context-5.3.14.jar:5.3.14]
at org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup.start(DefaultLifecycleProcessor.java:356) ~[spring-context-5.3.14.jar:5.3.14]
at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
...
集成方式如下:
简书地址:https://www.jianshu.com/p/55cbce0ecb16
明明之前一直都是好的,为什么这次就突然报错了呢?
本着遇事不决找版本的思路看了看, SpringBoot 2.6.2,会不会是版本的问题?改成 2.5.8 果然一切正常,真相大白,就是我们的老朋友,版本的问题。
那么为什么升到 2.6.2 就不行了呢?版本的具体什么问题导致了错误的出现?
这时我想到了之前在 b 站看到的一个 gradle 大佬给 maven 修 bug 的视频:https://www.bilibili.com/video/BV1vt411g7F5
没错,这个通过找不同从而快速定位问题的方法,我称之为对比调试法 ,对版本问题应该是特效工具了。
这就来操作一波(其实早前就操作过了,现在回来记录一下而已)。
二 对比调试
- 直接打开两个 IDE 窗口分别运行 2.5.8 和 2.6.2 版本的程序(注意端口不要冲突),启动观察结果,马上就能确定调试入口:
-
定位到
WebMvcPatternsRequestConditionWrapper.getPatterns()
方法,打个断点重新启动: -
找到报 NPE 的地方了,是这个 Wrapper 里的成员变量 condition,上下翻动可知只有构造方法能创建它,那么结合左边的调用栈来看看谁创建了这个 Wrapper :
从右边窗口可知有两个 Handler 创建了这个 Wrapper,具体是哪个呢?暂时不得而知,那么不妨先看看左边的调用栈,可以得知错误出现的大致上下文是:在对 RequestHandler 进行排序的时候根据 Condition 来排,结果这个 condition 是 null 的,所以就报 NPE 了。
-
那么是谁调用了排序呢,跳过 java.util 的步骤,从调用栈上往下找:
-
从以上的分析不难看出, 上图中的
toRequestHandler()
这个方法有很大的嫌疑,打个断点对比一下:简单分析可以看出,这个
WebMvcRequestHandlerProvider
里的私有属性handlerMapping
中的数据就已经不同了。那依旧老样子,追踪数据来源:这个requestHandler()
方法又由谁调用的呢?是DocumentationContextBuilder.withDefault()
,如下。 -
老样子打上断点重启。从下图可知,这个
handlerProvider
成了头号嫌疑,而它是AbstractDocumentationPluginsBootstrapper
的一个属性。从开头的报错信息到这里可能链路有点长了,重新梳理一下就是:这个 provider 提供的 handlerMapping 中的 mappingRegistry 里,名为 registry 的键值对中,key(类型是 RequestMappingInfo)里面的 patternsCondition 有问题,SpringBoot 2.6 环境中它为 null,2.5 则有值。 -
那是不是应该看看这个 Bootstrapper 的创建逻辑在两个版本之间有什么不同呢?不错,继续翻调用栈,buildContext, 再翻,可以来到它的子类
DocumentationPluginsBootstrapper
的start
方法。没错,它实现了 Spring 的SmartLifecycle
接口,所以它在 Spring 加载并初始化完 bean 后执行 start 中的逻辑(当然这是个题外话)。handlerProvider
在这个类的构造器中被注入。如你所见,这个类加了@Component
注解,是个被 Spring 托管的类。所以根据注入的基本原理,可以到这个 provider 的类中看看。 -
如下图,这个 Provider 是个接口,惯例找到它的实现:
这回又回到了这里,发现 handlerMappings 是由注入生成的(如果够敏感,第一次到这里就应该能发现)。记得刚才梳理的结果吗?下一步就是
handlerMappings
里的mappingRegistry
里的registry
里的 key ,忘记的可以往上翻翻第 6 步。 -
handlerMappings
所属类是RequestMappingInfoHandlerMapping
,mappingRegistry
不在此类而在它的抽象父类AbstractHandlerMethodMapping
中, 是一个内部类的实现。这个方法叫
register
,大概是往 mapping 中注册访问路径和访问规则的功能,那进一步追踪 mapping 的由来。 -
可以找到
register
的两个调用方:AbstractHandlerMethodMapping.registerMapping()
与AbstractHandlerMethodMapping.registerHandlerMethod()
,通过打断点大法可得知走了后者的方法。然后找到这个方法的真正调用者: �那段注释的意思大致就是要使用
getBuilderConfiguration()
的值去设置RequestMappingInfo
里的某个东西, 用来匹配这个 info 里设置HandlerMapping的逻辑,这非常重要,例如对于使用基本匹配的 PathPattern 或 PathMatcher 来说非常重要。好像看不出和我们的目标有什么联系?没关系,接着往下调试吧。 -
可以看到还有一个方法调用了这个
registerHandlerMethod
,而且是通过 lambda 表达式的方式调用的:通过断点的方式可以知道这个 mapping 里的 patternsCondition 是空的,继续找 mapping 的由来。
-
下面这段代码比较复杂,不过只需要理清楚数据来源就行了,目的是什么不用管。
追踪到 inspect 方法, 发现是个函数式接口,其实真正用的是上一步传进来的 lambda 表达式。其核心是
getMappingForMethod(method, userType)
这个方法。 -
进入方法,关键分支打上断点:
对比左右两边,发现 create 出来的 info(就是我们要找的 mapping)里面的值不一样。左边的
pathPatternsCondition
有值而patternsCondition
为空, 而右边的正好相反。 -
一路追踪,到达
createRequestMappingInfo(requestMapping, codition)
这个方法,直到这里,两边的入参都是相同的。但是,通过对比发现,config 里的值不同。左边的是 SpringBoot 2.6.2,其中
patternParser
是有值的,值为PathPatternParser
,而右边 SpringBoot 2.5.8 的版本里有值的是pathMatcher
,其值为AntPathMatcher
。通过万能的搜索引擎(或者经验丰富的同学已经知道了)可以得知,这是 SpringBoot 解析与匹配路径的策略。那么到了这一步,其实我们已经找到那个报错的问题的根本原因了。 -
出现问题的根本原因就是:SpringBoot 在 2.6 中改掉了路径匹配策略。这点可以通过翻 SpringBoot 项目的 Release Note 得知:
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes
如下图:
解决方案也很简单,如果项目求稳,不需要 2.6 的新特性,直接降到旧版本的 SpringBoot 使用;如果想尝鲜,但是对匹配策略不感冒的,可以通过在 Spring 配置文件中设置
spring.mvc.pathmatch.matching-strategy
为ant-path-matcher
即可,这点在官方文件中也提到了。
三 调试心得
- 对比调试法对版本出现差异的问题调试起来有很强的针对性,可以作为特效工具使用;
- 哪怕是对于不清楚运作逻辑的代码,只要咬紧线索,深入挖掘,还是能够找到问题所在的;
- 调试要有“不择手段”的精神;
- 平时多了解热门工具的版本及其新特性,多关注官方信息,可以简化很多不必要的开销。(比如事先知道 SpringBoot 2.6 的改动,那么出现问题的时候就会多一个心眼,明白大概是哪个更新导致了错误的出现)