乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 多模块项目深入

    • Reactor构建顺序
    • 继承与聚合组合实践
    • 模块间依赖
  • 第2章 插件体系深入

    • 插件目标goal
    • execution与自定义绑定
    • pluginManagement
  • 第3章 Profile高级应用

    • Profile激活机制
    • Profile与Spring Profile区别
  • 第4章 部署与分发

    • distributionManagement
    • mvn deploy
    • settings.xml认证配置
  • 第5章 CI/CD集成

    • Maven与持续集成
  • 第6章 自定义插件开发

    • 自定义插件开发
  • 第7章 高级依赖管理

    • 快照版本机制
    • 依赖分析工具
  • 第8章 仓库管理深入

    • 仓库组与路由

模块间依赖

本章承接"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 的检测。常见的重构策略:

  1. 抽取公共接口层:把 A 和 B 共同依赖的接口放到第三个模块 common-api 中
  2. 事件驱动解耦:用消息队列或事件总线替代直接的方法调用
  3. 依赖倒置:让高层模块依赖抽象接口,底层模块实现接口

生活类比:乐高积木的拼接规则

想象你有一套飞翔科技定制的乐高积木:

  • 模块间依赖:积木块 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}。原因有二:

  1. 当父 POM 版本升级为 1.1.0-SNAPSHOT 时,硬编码的 1.0.0 会导致模块 A 去本地仓库找旧版本的模块 B,而不是构建当前目录中的新版本
  2. 如果本地仓库中没有 1.0.0 版本的模块 B,构建会直接报错 Could not find artifact

误区三:循环依赖可以通过 -Dmaven.test.skip=true 绕过

错误认知:"构建报错是因为测试阶段循环依赖检测太严格,我跳过测试应该就能编译。"

纠正:循环依赖检测发生在 Reactor 初始化阶段,远在生命周期开始之前。无论你是否跳过测试、跳过编译、甚至只执行 mvn validate,Maven 都会先构建模块依赖图并检测循环。没有任何 Maven 参数可以绕过循环依赖检测,因为允许循环依赖会导致构建顺序无法确定,产物版本不可复现。


小结

多模块项目中的模块间依赖,本质上是用 GAV 坐标在子模块之间建立有向边。${project.version} 是保持版本一致性的关键约定,循环依赖是架构层级的致命缺陷,必须通过重构而非绕过方式解决。理解模块间依赖的声明方式和 Reactor 的排序逻辑,是设计可维护多模块架构的基础。

本章与全局的关系:本章讲解了模块间如何相互引用。下一章"继承与聚合组合实践"将把这些概念整合起来,展示一个真实项目中父 POM 如何同时扮演"继承模板"和"聚合根"的双重角色。

上一页
继承与聚合组合实践