Maven父POM:循环依赖

6
我们有一个模块化项目,大约有10个组件:
parent
 +- artifact1
 +- artifact2
 +- artifact3
 +- ...
 +- artifact10

此外,一些工件之间存在相互依赖关系:

artifact1
 +-> artifact2
 +-> artifact3
 +-> ...
 +-> artifact10

artifact2
 +-> artifact3

artifact4
 +-> artifact3

artifact4
 +-> artifact5

artifact5
 +-> artifact6

我们当前的设置如下:
  • parent是一个包含父POM的构件。
  • 这个父POM在中定义了所有必要的依赖项(如Spring,JPA等)。
  • 我们所有的构件也在中定义。
  • 我们的构件将父构件作为 - 显而易见的 - 父级引用。
  • 只有父POM定义版本。所有其他POM都不定义。

我们使用三个数字的版本控制方案:

<major version>.<minor version>.<patch level>

例如:

0.1.0-SNAPSHOT (a young artifact in development)
0.1.0 (the same artifact once it has been released)
0.1.1 (the same artifact after a hotfix)

问题:
一旦我们更改构件的版本(例如:0.1.0 => 0.1.1),我们的父构件版本(12.7.3)需要更新,因为它引用了旧的构件版本(0.1.0)。由于我们在父POM中更改了此引用(0.1.0 => 0.1.1),所以我们还需要增加父POM的版本(12.7.3 => 12.7.4)。现在,我们的构件仍然引用之前的父版本(12.7.3),即我们需要再次更新它... 这是循环的。
如何最好地解决这种循环的父子关系?我们可以从父POM中删除自己的依赖项,并在所有其他构件的POM中定义它们的版本,但这意味着一旦依赖项发生更改,我们就需要更新所有构件。
编辑
一个包含我们构件的简化目录结构:
.
├── [api:0.14.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java ...
│       │   └── webapp ...
│       └── test
├── [dao:1.21.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [parent:0.11.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [pdf-exporter:0.2.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [docx-exporter:0.3.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
├── [exporter-commons:0.9.0-SNAPSHOT]
│   ├── pom.xml
│   └── src
│       ├── main ...
│       └── test ...
└── [security:0.6.0-SNAPSHOT]
    ├── pom.xml
    └── src
        ├── main ...
        └── test ...

构件目录(用方括号括起来,并附有构件版本)相互独立,即它们仅在一个公共根目录(“.”)中为了方便而存在。每个构件都有自己的 Git 存储库。“api”是部署在应用服务器上的构件。所有构件都像这样引用“parent”(在开发期间):

<parent>
    <groupId>com.acme</groupId>
    <artifactId>parent</artifactId>
    <version>0.11.0-SNAPSHOT</version>
</parent>

<artifactId>api</artifactId>
<version>0.14.0-SNAPSHOT</version>

场景:

  • exporter-commons进行了更新: 0.9.0-SNAPSHOT => 0.9.1-SNAPSHOT。
  • docx-exporter和pdf-exporter引用不带版本号的exporter-commons,即无需更改。
  • 需要更新parent以反映exporter-commons的更新: 0.11.0-SNAPSHOT => 0.12.0-SNAPSHOT。

问题: api:0.14.0-SNAPSHOT引用parent:0.11.0-SNAPSHOT。然后将api:0.14.0-SNAPSHOT更新为引用parent:0.12.0-SNAPSHOT。api:0.14.0-SNAPSHOT变为api:0.15.0-SNAPSHOT。但是parent:0.12.0-SNAPSHOT中的pom.xml引用了api:0.14.0-SNAPSHOT。 =>恶性循环。

(注意:这里使用的构件名称仅为简化起见.)


这是一个由模块组成的大型项目吗?你能否将其简化为三个构件,以便更容易理解 :) - MariuszS
不,这不是一个多模块项目。所有的构件都在它们自己的仓库中。我会添加一个简化的构件结构示例。请给我一些时间。 ;) - Alexander Müller
3个回答

1
Maven中一个主要的困惑源是parent pom实际上包含了两种不同类型的关系:
- 父子关系: - 每个子模块内部声明一次,使用其标签 - 继承pom.xml内声明的插件、依赖、属性、版本等内容 - 聚合器-子模块关系: - 在顶层pom中通过标签声明一次 - 通过命令行传递目标(例如mvn clean install)会传递到子模块
如果所有模块保持相同的版本,则此区别无关紧要(即始终从顶层执行发布)。但是,一旦版本开始分裂(例如仅发布一个子模块而不发布另一个子模块),则需要为每个任务创建2个单独的pom。
project/
  parent/
    parent_pom.xml      # declare dependency versions as ranges [0.1.0, 0.2.0)
  children/
    aggregator_pom.xml  # <modules> section lists api/dao/etc
    api/
      pom.xml           # declare parent_pom as parent
    dao/
      pom.xml           # declare parent_pom as parent

为什么需要这种复杂的结构?
为什么不采用MariuszS建议的在顶级父级使用范围?
想象一下其中一个基本组件,比如api非常稳定。如果可以避免,您不希望重新构建或重新发布它。
同时,假设另外两个相互依赖的组件,如pdf-exporterdocs,您经常发布和/或分支,因此经常更改版本范围:0.1.x -> 0.2.x -> 0.3.x等。
然后,您将被迫经常修改和发布父POM以反映pdf-exporterdocs之间的关系,但您不一定希望发布api,因为它不关心这些更改。因此需要将父级放到一边,确保发布它不会触发api子模块的不必要重新发布。

1

建议

为了简化依赖配置,请使用 版本范围

例如,组件A需要版本为0.1.0的组件B。将依赖配置为范围<version>[0.1.0, 0.2.0)</version>

这意味着A需要版本大于或等于0.1.0且小于0.2.0B(因此所有热修复都适用于此组件)。

这有助于减少工作量,因为当热修复发布时,无需更改组件A的依赖项。只需重新构建父项目,已经修复的B将附加到项目A中。

这种技术要求在发布热修复时同时发布父项目。 父项目是指带有库的WAR或EAR,或包含所有组件的分发存档。

更多信息:3.4.3. 依赖版本范围

太好了,这看起来很有前途,谢谢。我会在接下来的几天里研究一下它。之后我会接受你的答案。 - Alexander Müller
1
更简单的是:LATEST和RELEASE。 :) - Alexander Müller

0

如果有一个共同的根模块(“父级”),所有其他模块(“子级”)都依赖于它,那么父模块不应该依赖于任何一个子模块。

这是因为Maven的工作方式相当简单:要构建一个模块,您首先需要构建所有该模块所依赖的模块(使用传递闭包模型),但这些模块尚未构建完成。如果存在循环依赖关系,就会出现这样一种情况:为了构建A,必须首先构建A。这很糟糕,显然无法发挥有效的作用。

但是,您还可以使父模块拥有其子模块成为子模块。这是一种不同的、不可继承的关系,它导致子模块在超级模块之后构建。这是可行的。让超级模块的子模块除了简单的包含关系外,与超级模块没有其他关系也是完全合理的。

简而言之,无论您如何在磁盘和存储库中安排模块,请不要引入循环依赖。(有人认为循环依赖应该逻辑上将一组模块转换为单个模块,因为这是合理定义proper-containment运算符操作的唯一方式。我不确定我完全同意这一点,但它并非完全错误...)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接