URL
date
AI summary
slug
status
tags
summary
type

现象

上周碰到一个很诡异的问题:在某个时间点之后构建并发布的应用,启动过程中都会出现找不到com.jayway.jsonpath.Configuration类的异常,导致容器启动失败。
notion image

分析

ClassNotFound?

从表象上来看,就是类加载的时候找不到这个com.jayway.jsonpath.Configuration类。这个类是在com.jayway.jsonpath:json-path包里的。
我们找到这个启动失败的fatjar,查看了里面的依赖包,发现确实没有找到json-path这个包。而在之前构建出来的fatjar里,都发现了json-path这个包。
所以,ClassNotFound这个异常到这里已经清楚了,确实是classpath下找不到对应的类,因为对应的jar包都没有依赖进来。但是令人疑惑的是,为什么json-path这个包突然就依赖不进来了?
首先,我观察到这个包并不是在最终打包的项目里直接引用进来的,而是通过其他的jar包传递依赖进来的。那么,问题可能出在传递依赖上。我尝试手动直接依赖一下,发现问题就临时解决了。

传递依赖失效?

但是,本着“要把每个问题刨根问底”的原则,我还是尝试找到那个导致传递依赖失效的那个“变量”。因为我们内部为了开发效率,服务间依赖的api包都是SNAPSHOT的,猜测可能是因为SNAPSHOT包做了调整导致的。于是看了git变更记录以及maven私服的jar包更新记录。终于找到了一个不太起眼的变更(其实起初我也是没在意的,在反复查找没有进展之后终于再次定位)。
notion image
但是为什么多引入了一个包之后就出现问题了呢?由于我们的项目依赖过于复杂,不便于说明问题,我会用自己构造的最小化的问题场景来讲解。(其实造这个最小化的场景非常的费劲,因为你必须得知道问题原因之后,才知道怎么去造场景。而这个过程大概花了4天左右的时间。)
构造的最小化问题场景已经上传到Github上了,大家可以在本地重现问题
dependency-problem
zhuchao941Updated Jul 13, 2023

maven依赖冲突解决机制

首先,我们知道maven有解决依赖冲突的机制,比如A和B都依赖了log4j2,而我的项目依赖了A和B,那么最终log4j2用谁依赖的版本,这个就是依赖冲突机制需要解决的事情。关于这个机制,我拿这几天阅读源代码的成果来分享一下大致的流程:
  1. 首先,maven会通过待编译项目下的pom.xml文件递归解析整个依赖有向图(为什么不是依赖树呢,因为可能存在循环,包括多个节点指向同一个节点等非树的特性)
  1. maven会对图中的每个节点计算它的入度(indegree)和深度(depth),用来进行拓扑排序。拓扑排序的顺序,决定了后续依赖包的处理顺序
  1. 如果说依赖有向图中存在环,那么拓扑排序完之后肯定还有节点没有被解析出来。这些节点将会按照depth asc, indegree asc排序后取出第一个节点,再进行拓扑排序。直到所有节点都被解析完。
  1. 对于排完序的依赖包,依次处理它的依赖冲突
    1. 从root节点递归遍历所有的依赖包,找出当前依赖包所有的依赖来源。不过在递归遍历的时候,有一个限制,就是当前这个jar包必须被“认证过”才可以递归它的依赖包,不然就直接终止。而“认证过”有2个来源:1是解决过冲突,2是循环依赖里的直接循环jar(不过为什么要有这个限制还不是很理解,有了解的同学还望可以在评论区指教)
    2. 对所有的依赖来源做pk,默认规则现在是取depth短的,如果depth一样,那就哪个先被递归到就先用哪个(这个应该是跟声明顺序相关)

循环依赖叠加上其他因素

而上面的demo我们构造的场景是
  1. dependency3和dependency4循环依赖了。导致循环依赖链路(dependency3、dependency4、json-path)使用了depth asc, indegree asc的顺序来排序
  1. 项目直接依赖了spring-boot-starter-test,而spring-boot-starter-test里引入了json-path依赖包。所以json-path的minDepth很小(等于2)
  1. 依赖spring-boot-starter-test这个包的scope是test
下面这个有向图展示了整条依赖链路,以及对应节点的入度和深度。可以结合下图和上面的流程一起看。相信应该还是比较清晰的
notion image

解决方案

既然知道了原因,那么我们解决这个问题的思路自然也有了
  1. 破坏循环依赖,dependency4不再依赖dependency3即可
  1. 让json-path的minDepth不是最小的,比如减少左侧的依赖深度,比如dependency1直接依赖dependency3
  1. 把spring-boot-starter-test的scope改成compile

排查过程及工具分享

整个过程的分析其实是挺费精力的。
  1. 因为原先对于maven处理依赖冲突的机制不是太了解
  1. 因为对于maven插件的代码不熟悉
  1. 因为对于一些算法掌握的也比较薄弱(比如对于有向图的拓扑排序)
不过还好借助了mvnDebug,我可以无数次的debug源代码,并且一行行的跟踪执行过程。也借助了maven的offline模式(因为我们都是SNAPSHOT包,并且由于出现这个循环依赖的问题,我已经“责令”团队尽快把存在循环依赖的地方给干掉)。

offline模式

maven的offline模式,也就是不和远程仓库交互,包括SNAPSHOT的计算以及拉取不存在的包。只需要在mvn命令后面加参数-o即可,比如mvn dependency:tree -o

mvnDebug

debug maven插件,只需要把命令mvn改成mvnDebug即可,比如mvnDebug dependency:tree -o,默认监听端口8000,这个时候再用一个远程debug连接到端口8000即可。当然前提是你要准备好插件对应的源代码。
这里我是下载了maven-dependency-plugin项目进行的debug,需要保证插件和源代码用的是同一个版本。另外,也要保证maven是同一个版本。插件项目里可以通过修改properties来调整maven版本
notion image

总结

产生这个问题的原因其实是因为团队没有好好的执行我们的开发规范:api包要保证最小化依赖。而现实的执行存在2个问题:
  1. 习惯于把api引在api里,比如我a服务需要调用b服务的接口,按规范我们应该把b服务的api依赖在a服务的实现层即可,而不应该直接在api层。这样很容易导致循环依赖。
  1. 把一些比较重量级的包引在api里,比如导致这次问题的某个api里引入了一个starter包,就导致了依赖了非常多的jar包,然后这些jar包都被包含在了循环依赖的链路里。
所以,好好执行开发规范可以避免很多不必要的问题。但是,人,往往在踩了坑之后才记得更深刻。
通过IntelliJ IDEA 创建可执行jar包线上问题分析——canal-adapter数据同步不全问题排查