提到maven循环依赖是maven 解析的痛点。循环依赖的存在会使maven build阶段出现build fail严重的会出现死循环,进而导致maven栈溢出。这篇文章我们将从最简单的maven循环依赖讲起,结合笔者的项目经历对循环依赖进行分析
最简单的循环依赖
Sample 项目
为了更好的理解maven循环依赖。我们创建一个最简单的sample项目:maven-test。这是一个maven多模块项目,包含a-component和b-component这两个模块。分析一下这个项目的pom.xml和java类,两个模块的依赖关系是a->b->a
,两个模块中的类的依赖关系是A->B->A
。 这就是最简单的循环依赖的case。
maven-test(parent) pom.xml
4.0.0
org.example
maven-test
1.0-SNAPSHOT
a-component
b-component
pom
org.example
a-component
1.0-SNAPSHOT
org.example
b-component
1.0-SNAPSHOT
org.codehaus.mojo
build-helper-maven-plugin
3.2.0
a-component pom.xml
maven-test
org.example
1.0-SNAPSHOT
4.0.0
a-component
org.example
b-component
a-component A.java
public class A {
private B b;
}
b-component pom.xml
maven-test
org.example
1.0-SNAPSHOT
4.0.0
b-component
org.example
a-component
b-component B.java
public class B {
private A a;
}
使用maven定位最简单循环依赖
此时我们run一下maven clean install
命令。通过观察log,发现对于多模块项目的各个模块,maven不仅能够帮我们判断项目中是否存在循环依赖,更会帮我们找到具体的循环依赖。
LM-SHC-16507782:maven-test xianghan$ mvn clean install
[INFO] Scanning for projects...
[ERROR] [ERROR] The projects in the reactor contain a cyclic reference: Edge between 'Vertex{label='org.example:b-component:1.0-SNAPSHOT'}' and 'Vertex{label='org.example:a-component:1.0-SNAPSHOT'}' introduces to cycle in the graph org.example:a-component:1.0-SNAPSHOT --> org.example:b-component:1.0-SNAPSHOT --> org.example:a-component:1.0-SNAPSHOT @
[ERROR] The projects in the reactor contain a cyclic reference: Edge between 'Vertex{label='org.example:b-component:1.0-SNAPSHOT'}' and 'Vertex{label='org.example:a-component:1.0-SNAPSHOT'}' introduces to cycle in the graph org.example:a-component:1.0-SNAPSHOT --> org.example:b-component:1.0-SNAPSHOT --> org.example:a-component:1.0-SNAPSHOT -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/ProjectCycleException
最简单循环依赖的解决思路
解决循环依赖的方式就是打断循环依赖链。无论是使用build-helper-maven-plugin的方式或者项目重构,思路都是抽出耦合部分代码创建为新的component,之后将a-component和b-component的依赖关系转向这个新的component即可。下面我们介绍一下使用build-helper-maven-plugin进行解决的方法。
使用build-helper-maven-plugin
- 我们在a-component和b-component之外,创建一个新的模块c-component。
- 修改a-component和b-component的pom.xml,将他们的依赖关系从彼此转向c-component。此时项目中各模块的依赖关系从
a->b->a
改为了a->c
,b->c
,已经不存在循环依赖的问题,但是此时会发现a-component和b-component会由于找不到类文件而报错。 - 在c-component中添加build-helper-maven-plugin插件,通过build-helper-maven-plugin整合a-component和b-component中耦合部分的代码。
org.codehaus.mojo
build-helper-maven-plugin
3.2.0
add-source
generate-sources
add-source
如何发现循环依赖
在一个复杂系统中,我们需要同时release几千个libraries。这些libraries由于数量众多,分别属于不同的部门, 因此很难管理,没有人清楚这些libraries中是不是存在循环依赖。
为了release工作的顺利开展,我们需要提前确认这些libraries中是否存在循环依赖,如果存在循环依赖的话,我们需要定位出这个循环依赖。
解决思路
libraries之间的依赖关系可以通过有向图这种数据结构来描述。所以,我们现在的任务就可以通过三个步骤来解决。
得到有向图
有向图的基本元素是节点和弧。我们来规定一下在我们的case中节点和弧分别指代的内容。
节点:每一个需要被release的library都是图中的一个节点。
弧:项目间的依赖关系,从依赖方指向被依赖方。
接下来就是通过计算来得到有向图了。经过各种尝试后,笔者最后通过下面这种方法得到以每一个节点为起点的弧的信息。
-
git clone
下载这些library的源码 - 运行
mvn dependency:list
获取library的依赖关系
至此,我们已经得到了由libraries和它们之间的依赖关系组成的有向图了。
判断有向图是否有环
我们假设上一步得到的有向图如图1所示。检查有向图是否有环的算法已经比较多了,我们简单介绍其中的一个办法,供大家参考。
- 求出图中所有节点的出度。
- 将所有出度==0的节点放入队列
-
当队列不空时进入循环,弹出队首节点h,把节点h所依赖的节点的出度减1,如果这些节点的出度变为0,则将这些节点入队。队列为空则退出循环。
-
循环结束时判断已经访问过(进入过队列)的节点数是否等于 n。等于 n 说明全部节点都被访问过,无环;反之,则有环。
从上图看,还剩下Project1,Project2, Project 3 此时依赖有环。
有环的话,我们需要找出其中的一个环
根据上一步的计算结果,已经确定在这些libraries中存在循环依赖,下一步的任务就是找到循环依赖中的一个环,抽丝剥茧的解决项目中的循环依赖。
通过在上一个小节中的知识,我们从有向图的正向(出度为0)出发,可以排除掉一批进入过队列的节点以及与之对应的弧。同样的,我们也可以从有向图的反向(入度为0)出发,排除掉另一批进入过队列的节点和对应的弧。
此时,我们沿着任意一个节点的弧走下去,无论这个节点本身是否处于一个环中,沿着它出发必然能够找到一个环。算法如下:
- 创建空的队列Q。
- 任选一个节点。将节点标志成已读之后放入到队列Q中。
-
以上次进入队列的节点H为起点,任选一条弧,找到弧的终点的节点T。检查T的节点标志是否为已读,如果是,则进入步骤4;否则,将节点标志成已读之后放入到队列Q中,重复步骤3。
-
将队中元素依次出队并与T进行比较,直到出队元素为T。此时,T以及队列中剩余的元素构成循环依赖。