模块间依赖
本章承接"Reactor 构建顺序",深入讲解多模块项目中子模块之间如何相互引用。入门教程已覆盖模块对第三方库的依赖,本章聚焦模块对模块的依赖——这是将单体项目拆分为多模块后必须跨越的第一道门槛。
核心机制
在多模块项目中,子模块之间不是通过文件路径或 IDE 的"项目引用"来关联的,而是通过 Maven 的**坐标系统(GAV)**来相互依赖。这种依赖方式与依赖第三方库(如 Spring、JUnit)在语法上完全一致,但在语义上有重要区别:模块间依赖意味着 Reactor 必须建立构建顺序,且被依赖模块的产物必须先行安装到本地仓库。
模块间依赖的声明方式
子模块 A 依赖子模块 B 时,在 A 的 pom.xml 中声明:
<dependencies>
<dependency>
<groupId>com.feixiang</groupId>
<artifactId>feixiang-module-b</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
这里的关键是 ${project.version}。由于所有子模块通常共享同一个父 POM 中定义的版本,使用 ${project.version} 可以确保:当父 POM 的版本升级时,所有模块间的依赖版本自动同步,无需逐个修改。如果硬编码写死 1.0.0,版本升级时就会遗漏,导致模块间引用的是本地仓库中的旧版本,引发"修改了代码却不生效"的诡异问题。
模块间依赖与第三方依赖的本质区别
| 维度 | 模块间依赖 | 第三方依赖 |
|---|---|---|
| 坐标来源 | 同一多模块项目中的其他子模块 | 中央仓库或私有仓库中的外部项目 |
| 版本管理 | 通常用 ${project.version} 统一 | 在父 POM 的 <dependencyManagement> 中锁定 |
| 构建影响 | 影响 Reactor 的拓扑排序 | 不影响 Reactor 排序(已存在于仓库) |
| 产物来源 | 由 Reactor 在本轮构建中生成 | 从仓库下载或本地缓存 |
| 修改可见性 | 修改后需重新 install 才能被依赖方感知 | 修改后需发布到新版本才能感知 |
循环依赖:多模块架构的致命陷阱
循环依赖是指模块 A 依赖模块 B,同时模块 B(直接或间接)依赖模块 A。Maven 的 Reactor 在解析依赖图时能够检测循环依赖,并在构建开始时直接报错终止,而不是陷入无限循环或产生不可预期的构建结果。
循环依赖的典型报错:
[ERROR] The projects in the reactor contain a cyclic dependency:
[ERROR] com.feixiang:feixiang-service -> com.feixiang:feixiang-core-api -> com.feixiang:feixiang-service
解决循环依赖的唯一正确方式是重构代码,而不是试图绕过 Maven 的检测。常见的重构策略:
- 抽取公共接口层:把 A 和 B 共同依赖的接口放到第三个模块
common-api中 - 事件驱动解耦:用消息队列或事件总线替代直接的方法调用
- 依赖倒置:让高层模块依赖抽象接口,底层模块实现接口
生活类比:乐高积木的拼接规则
想象你有一套飞翔科技定制的乐高积木:
- 模块间依赖:积木块 A 的凸起必须插入积木块 B 的凹槽。你不能把 A 和 B 同时拿起来随意拼接——必须先完成 B 的组装,才能知道凹槽在哪里,然后才能组装 A。
- 第三方依赖:你还需要一些标准乐高零件(如轮子、窗户),这些零件已经从工厂生产好并存放在仓库里,你随时可以直接取用。
- 循环依赖:你设计了一套积木,要求"必须先装车头才能装车身",同时又说"必须先装车身才能装车头"——这套设计图纸本身就是矛盾的,工厂(Maven)会拒绝生产。
图示
上图展示了三种模块关系:左侧是正常的树状依赖,所有箭头单向向下,Reactor 可以顺利排序;中间是循环依赖,箭头形成闭环,Maven 会直接拒绝构建;右侧是解耦后的结构,通过引入 common-api 抽象层,打破了循环,同时保持了功能完整性。核心原则:模块依赖图必须是有向无环图(DAG),任何闭环都是架构缺陷。
完整示例
场景
飞翔科技的电商中台系统已经拆分为 3 个模块。后端工程师小崔负责 feixiang-service,他需要调用 feixiang-core-api 中定义的 OrderDTO 类。同时,架构师白歌发现 feixiang-core-api 中有一个工具类 ServiceHelper 反向调用了 feixiang-service 中的业务方法,形成了潜在的循环依赖。
操作前的配置/项目状态
项目目录结构:
feixiang-parent/
├── pom.xml
├── feixiang-core-api/
│ ├── pom.xml
│ └── src/main/java/com/feixiang/dto/OrderDTO.java
├── feixiang-service/
│ ├── pom.xml
│ └── src/main/java/com/feixiang/service/OrderService.java
└── feixiang-web-ui/
├── pom.xml
└── src/main/java/com/feixiang/web/OrderController.java
feixiang-service/pom.xml 中正确声明了对 core-api 的依赖:
<dependencies>
<dependency>
<groupId>com.feixiang</groupId>
<artifactId>feixiang-core-api</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
但 feixiang-core-api 中有一个类错误地直接引用了 feixiang-service:
// feixiang-core-api/src/main/java/com/feixiang/util/ServiceHelper.java
package com.feixiang.util;
import com.feixiang.service.OrderService; // ❌ 错误:底层模块依赖上层模块
public class ServiceHelper {
public static void process() {
OrderService service = new OrderService();
service.cancelOrder(null);
}
}
对应的 feixiang-core-api/pom.xml 中错误地声明了:
<dependencies>
<dependency>
<groupId>com.feixiang</groupId>
<artifactId>feixiang-service</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
操作步骤
小崔在 feixiang-parent 目录下执行构建:
mvn clean install
操作结果
Maven 在解析 Reactor 依赖图时检测到循环依赖,直接报错:
[ERROR] The projects in the reactor contain a cyclic dependency:
[ERROR] com.feixiang:feixiang-service -> com.feixiang:feixiang-core-api -> com.feixiang:feixiang-service
[ERROR] @
[ERROR] The build cannot continue as the dependency graph is not acyclic.
分析:
feixiang-service依赖feixiang-core-api(正常:上层依赖下层)feixiang-core-api又依赖feixiang-service(异常:下层反向依赖上层)- Reactor 构建了一个有向图,发现从
service出发可以回到service,判定为循环依赖
修复方案:白歌将 ServiceHelper 中依赖 OrderService 的逻辑抽取到 feixiang-service 中,删除 feixiang-core-api 对 feixiang-service 的依赖声明。core-api 只保留 DTO 和接口,不依赖任何业务实现模块。修复后重新执行 mvn clean install,构建成功,Reactor Build Order 为:
[INFO] Reactor Build Order:
[INFO] feixiang-parent
[INFO] feixiang-core-api
[INFO] feixiang-service
[INFO] feixiang-web-ui
易错点与常见问题
误区一:模块间依赖可以用相对路径或文件系统引用
错误认知:"模块 A 和模块 B 在同一个父目录下,我直接在代码里 import ../module-b/... 就行了,不用在 pom.xml 里声明依赖。"
纠正:Maven 的模块间依赖必须通过 GAV 坐标在 pom.xml 中显式声明。源代码层面的文件路径引用在 IDE 中可能能编译(因为 IDE 有自己的项目模型),但在命令行执行 mvn compile 时,Maven 只认 pom.xml 中声明的依赖。没有声明的模块,即使物理上相邻,Maven 也不会将其加入 classpath。这会导致 CI/CD 流水线构建失败,而本地 IDE 却"一切正常"——是最典型的"本地能跑,线上报错"问题之一。
误区二:模块间依赖的版本可以随便写
错误认知:"模块 A 依赖模块 B,版本我写 1.0.0 还是 ${project.version} 都一样。"
纠正:在多模块项目中,必须使用 ${project.version}。原因有二:
- 当父 POM 版本升级为
1.1.0-SNAPSHOT时,硬编码的1.0.0会导致模块 A 去本地仓库找旧版本的模块 B,而不是构建当前目录中的新版本 - 如果本地仓库中没有
1.0.0版本的模块 B,构建会直接报错Could not find artifact
误区三:循环依赖可以通过 -Dmaven.test.skip=true 绕过
错误认知:"构建报错是因为测试阶段循环依赖检测太严格,我跳过测试应该就能编译。"
纠正:循环依赖检测发生在 Reactor 初始化阶段,远在生命周期开始之前。无论你是否跳过测试、跳过编译、甚至只执行 mvn validate,Maven 都会先构建模块依赖图并检测循环。没有任何 Maven 参数可以绕过循环依赖检测,因为允许循环依赖会导致构建顺序无法确定,产物版本不可复现。
小结
多模块项目中的模块间依赖,本质上是用 GAV 坐标在子模块之间建立有向边。${project.version} 是保持版本一致性的关键约定,循环依赖是架构层级的致命缺陷,必须通过重构而非绕过方式解决。理解模块间依赖的声明方式和 Reactor 的排序逻辑,是设计可维护多模块架构的基础。
本章与全局的关系:本章讲解了模块间如何相互引用。下一章"继承与聚合组合实践"将把这些概念整合起来,展示一个真实项目中父 POM 如何同时扮演"继承模板"和"聚合根"的双重角色。