对比调试法解决SpringBoot版本问题

一 起因

之前写一个小 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://lookoutldz.top/archives/springboot%E9%9B%86%E6%88%90%E6%96%B0%E7%89%88swagger2starter%E6%96%B9%E5%BC%8F

地址: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

没错,这个通过找不同从而快速定位问题的方法,我称之为对比调试法 ,对版本问题应该是特效工具了。

这就来操作一波(其实早前就操作过了,现在回来记录一下而已)。

二 对比调试

  1. 直接打开两个 IDE 窗口分别运行 2.5.8 和 2.6.2 版本的程序(注意端口不要冲突),启动观察结果,马上就能确定调试入口:
图 1
  1. 定位到 WebMvcPatternsRequestConditionWrapper.getPatterns() 方法,打个断点重新启动:

    图 2
  2. 找到报 NPE 的地方了,是这个 Wrapper 里的成员变量 condition,上下翻动可知只有构造方法能创建它,那么结合左边的调用栈来看看谁创建了这个 Wrapper :

    图 3

    从右边窗口可知有两个 Handler 创建了这个 Wrapper,具体是哪个呢?暂时不得而知,那么不妨先看看左边的调用栈,可以得知错误出现的大致上下文是:在对 RequestHandler 进行排序的时候根据 Condition 来排,结果这个 condition 是 null 的,所以就报 NPE 了。

  3. 那么是谁调用了排序呢,跳过 java.util 的步骤,从调用栈上往下找:

    图 4
  4. 从以上的分析不难看出, 上图中的 toRequestHandler() 这个方法有很大的嫌疑,打个断点对比一下:

    图 5

    简单分析可以看出,这个 WebMvcRequestHandlerProvider 里的私有属性 handlerMapping 中的数据就已经不同了。那依旧老样子,追踪数据来源:这个 requestHandler()方法又由谁调用的呢?是 DocumentationContextBuilder.withDefault() ,如下。

  5. 老样子打上断点重启。从下图可知,这个 handlerProvider 成了头号嫌疑,而它是 AbstractDocumentationPluginsBootstrapper 的一个属性。从开头的报错信息到这里可能链路有点长了,重新梳理一下就是:这个 provider 提供的 handlerMapping 中的 mappingRegistry 里,名为 registry 的键值对中,key(类型是 RequestMappingInfo)里面的 patternsCondition 有问题,SpringBoot 2.6 环境中它为 null,2.5 则有值。

    图 6
  6. 那是不是应该看看这个 Bootstrapper 的创建逻辑在两个版本之间有什么不同呢?不错,继续翻调用栈,buildContext, 再翻,可以来到它的子类 DocumentationPluginsBootstrapperstart 方法。没错,它实现了 Spring 的 SmartLifecycle 接口,所以它在 Spring 加载并初始化完 bean 后执行 start 中的逻辑(当然这是个题外话)。

    图 7
    仔细观察,我们要找的 handlerProvider 在这个类的构造器中被注入。如你所见,这个类加了 @Component 注解,是个被 Spring 托管的类。所以根据注入的基本原理,可以到这个 provider 的类中看看。

  7. 如下图,这个 Provider 是个接口,惯例找到它的实现:

    图 8

    这回又回到了这里,发现 handlerMappings 是由注入生成的(如果够敏感,第一次到这里就应该能发现)。记得刚才梳理的结果吗?下一步就是 handlerMappings 里的 mappingRegistry 里的 registry 里的 key ,忘记的可以往上翻翻第 6 步。

  8. handlerMappings 所属类是 RequestMappingInfoHandlerMappingmappingRegistry 不在此类而在它的抽象父类 AbstractHandlerMethodMapping中, 是一个内部类的实现。

    图 9

    这个方法叫 register ,大概是往 mapping 中注册访问路径和访问规则的功能,那进一步追踪 mapping 的由来。

  9. 可以找到 register 的两个调用方:AbstractHandlerMethodMapping.registerMapping()AbstractHandlerMethodMapping.registerHandlerMethod() ,通过打断点大法可得知走了后者的方法。然后找到这个方法的真正调用者: �

    图10

    那段注释的意思大致就是要使用 getBuilderConfiguration() 的值去设置 RequestMappingInfo 里的某个东西, 用来匹配这个 info 里设置HandlerMapping的逻辑,这非常重要,例如对于使用基本匹配的 PathPattern 或 PathMatcher 来说非常重要。好像看不出和我们的目标有什么联系?没关系,接着往下调试吧。

  10. 可以看到还有一个方法调用了这个 registerHandlerMethod ,而且是通过 lambda 表达式的方式调用的:

    图 11

    通过断点的方式可以知道这个 mapping 里的 patternsCondition 是空的,继续找 mapping 的由来。

  11. 下面这段代码比较复杂,不过只需要理清楚数据来源就行了,目的是什么不用管。

    图 12

    追踪到 inspect 方法, 发现是个函数式接口,其实真正用的是上一步传进来的 lambda 表达式。其核心是 getMappingForMethod(method, userType) 这个方法。

  12. 进入方法,关键分支打上断点:

    图 13

    对比左右两边,发现 create 出来的 info(就是我们要找的 mapping)里面的值不一样。左边的 pathPatternsCondition 有值而 patternsCondition 为空, 而右边的正好相反。

  13. 一路追踪,到达 createRequestMappingInfo(requestMapping, codition) 这个方法,直到这里,两边的入参都是相同的。

    图 14

    但是,通过对比发现,config 里的值不同。左边的是 SpringBoot 2.6.2,其中 patternParser 是有值的,值为 PathPatternParser ,而右边 SpringBoot 2.5.8 的版本里有值的是 pathMatcher,其值为 AntPathMatcher。通过万能的搜索引擎(或者经验丰富的同学已经知道了)可以得知,这是 SpringBoot 解析与匹配路径的策略。那么到了这一步,其实我们已经找到那个报错的问题的根本原因了。

  14. 出现问题的根本原因就是:SpringBoot 在 2.6 中改掉了路径匹配策略。这点可以通过翻 SpringBoot 项目的 Release Note 得知:

    https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.6-Release-Notes

    如下图:

    图 15

    解决方案也很简单,如果项目求稳,不需要 2.6 的新特性,直接降到旧版本的 SpringBoot 使用;如果想尝鲜,但是对匹配策略不感冒的,可以通过在 Spring 配置文件中设置 spring.mvc.pathmatch.matching-strategyant-path-matcher 即可,这点在官方文件中也提到了。

三 调试心得

  1. 对比调试法对版本出现差异的问题调试起来有很强的针对性,可以作为特效工具使用;
  2. 哪怕是对于不清楚运作逻辑的代码,只要咬紧线索,深入挖掘,还是能够找到问题所在的;
  3. 调试要有“不择手段”的精神;
  4. 平时多了解热门工具的版本及其新特性,多关注官方信息,可以简化很多不必要的开销。(比如事先知道 SpringBoot 2.6 的改动,那么出现问题的时候就会多一个心眼,明白大概是哪个更新导致了错误的出现)

你可能感兴趣的:(对比调试法解决SpringBoot版本问题)