Maven 详解(下)
在标准的 Maven 多模块项目中,<parent> 和 <modules> 不仅可以同时出现,它们是“黄金搭档”,同时存在才能完整实现父子工程的聚合与继承功能。
这两个标签共同构成了 Maven 多模块项目的骨架。理解它们的作用、位置和工作机制是掌握模块化开发的关键。
一、 <modules> 标签 (在父 POM 中) - 聚合 (Aggregation)
作用: 告诉 Maven:“我这个项目(父工程)是由哪些子模块组成的”。它实现了项目聚合。
位置: 父工程 (
packaging=pom) 的pom.xml文件中。语法:
<modules><module>relative/path/to/module1</module><module>relative/path/to/module2</module><!-- ... --> </modules><module>元素的值:- 这是一个相对路径,指向子模块目录的位置。
- 路径是相对于父 POM 文件所在目录的。
- 常见形式:
<module>module-common</module>(子模块与父 POM 同级)<module>../shared-lib</module>(子模块在父工程的上一级目录,较少见)<module>services/user-service</module>(更深层级的目录结构)
- 路径指向的是包含
pom.xml文件的子模块目录,而不是文件本身。
工作原理 (聚合机制):
- 当你在父工程根目录执行 Maven 命令时(例如
mvn clean install),Maven 首先会读取父 POM。 - Maven 发现
<modules>标签,就知道这是一个聚合项目。 - Maven 会遍历
<modules>列表中的每一个路径,找到对应的子模块pom.xml。 - Maven 分析所有子模块之间的依赖关系(通过查看每个子模块
pom.xml中的<dependencies>)。 - Maven 根据依赖关系自动计算出一个正确的构建顺序(有依赖的模块必须先构建)。
- Maven 按照计算出的顺序,依次对每个子模块执行指定的生命周期阶段。
- 当你在父工程根目录执行 Maven 命令时(例如
关键点:
- 聚合是可选的:你可以有一个父 POM 用于继承,但不包含
<modules>,这样你就不能在父目录下用一条命令构建所有模块。 - 聚合不等于继承:
<modules>只负责“把哪些模块一起构建”,它本身不提供任何配置继承。继承是由<parent>标签实现的。 - 独立性:即使没有
<modules>,你也可以单独进入某个子模块目录运行mvn命令来构建它。
- 聚合是可选的:你可以有一个父 POM 用于继承,但不包含
二、 <parent> 标签 (在子 POM 中) - 继承 (Inheritance)
作用: 告诉 Maven:“我的这个子模块要继承哪个父 POM 的配置”。它实现了配置继承。
位置: 每个需要继承的子模块的
pom.xml文件中。语法:
<parent><groupId>com.example</groupId><artifactId>my-parent-project</artifactId><version>1.0.0-SNAPSHOT</version><!-- 相对路径,告诉 Maven 如何找到父 POM 文件 --><relativePath>../pom.xml</relativePath> </parent>各个元素的含义:
<groupId>,<artifactId>,<version>: 这三个元素精确地定位了父 POM 的坐标。Maven 会根据这三个信息去查找父 POM。<relativePath>:- 最重要的属性之一。
- 它是一个相对路径,告诉 Maven 在哪里可以找到父 POM 的
pom.xml文件。 - 路径是相对于当前子模块的
pom.xml文件所在目录的。 - 默认值:
../pom.xml。这意味着 Maven 默认假设父 POM 在子模块目录的上一级目录。 - 何时需要显式声明:
- 当你的标准结构被打破时(例如,子模块不在父工程目录下)。
- 为了代码清晰,建议总是显式写出
<relativePath>。 - 如果父 POM 已经发布到远程仓库,而你希望优先从仓库下载而不是找本地文件,可以设置
<relativePath/>(空值)。
工作原理 (继承机制):
- 当 Maven 处理一个子模块的
pom.xml时,发现<parent>标签。 - Maven 会根据
<groupId>,<artifactId>,<version>和<relativePath>去定位并读取父 POM 文件。 - Maven 将父 POM 中的配置与子模块自身的配置进行合并。
- 继承的内容包括:
- 父 POM 的
groupId(子模块通常会继承,除非自己声明) - 父 POM 的
version(子模块通常会继承,除非自己声明) <properties><dependencyManagement><pluginManagement><repositories>/<pluginRepositories><build>中直接定义的配置(如果父 POM 不是在<pluginManagement>下定义插件)。
- 父 POM 的
- 子模块可以在自己的
pom.xml中覆盖或补充这些继承来的配置。
- 当 Maven 处理一个子模块的
关键点:
- 继承是可选的:一个子模块可以选择不继承任何父 POM。
- 单继承: Maven POM 只支持单一父类继承(就像 Java 类一样)。一个子模块只能有一个
<parent>。 - 链式继承: 继承可以形成链条。例如,
Child->Parent->GrandParent。Child会继承Parent和GrandParent的所有配置。 - BOM 导入也是继承的一种应用: Spring Boot 的
spring-boot-starter-parent本质上就是一个提供了大量默认配置的父 POM。
三、 <parent> 与 <modules> 的协同工作
虽然 <parent> 和 <modules> 分别位于不同的 POM 文件中,但它们共同协作,使多模块项目得以高效运行。
典型工作流程:
结构建立:
- 你在父工程的
pom.xml中用<modules>列出了所有子模块。 - 你在每个子模块的
pom.xml中用<parent>指向了父工程。
- 你在父工程的
构建触发:
- 你在父工程根目录执行
mvn install。
- 你在父工程根目录执行
Maven 执行过程:
- 步骤 1 (聚合): Maven 读取父 POM,发现
<modules>,知道了要构建module-common,module-service,module-web。 - 步骤 2 (依赖分析): Maven 解析每个子模块的
pom.xml,发现module-web依赖module-service,module-service依赖module-common。 - 步骤 3 (排序): Maven 计算出构建顺序:
module-common->module-service->module-web。 - 步骤 4 (继承与构建):
- 开始构建
module-common。由于它的pom.xml中有<parent>,Maven 加载父 POM 配置,合并后得到 Effective POM,然后执行install。 - 构建
module-service。同样,通过<parent>继承父 POM 配置,并且它的依赖module-common已经在本地构建好了(在~/.m2/repository或模块间直接引用),可以顺利编译。 - 构建
module-web。同理,继承父 POM,并成功依赖已构建好的module-service。
- 开始构建
- 步骤 1 (聚合): Maven 读取父 POM,发现
四、 总结对比表
| 特性 | <modules> | <parent> |
|---|---|---|
| 中文含义 | 模块 (复数) | 父级 |
| 主要目的 | 聚合 (Aggregation) - “一起构建哪些模块” | 继承 (Inheritance) - “从谁那里继承配置” |
| 所在位置 | 父工程的 pom.xml 中 | 子工程的 pom.xml 中 |
| 定义方向 | 自上而下 (父告诉世界它有哪些孩子) | 自下而上 (子告诉世界它的父亲是谁) |
| 内容 | 包含一个或多个 <module> 元素,每个是子模块目录的相对路径 | 包含 <groupId>, <artifactId>, <version> 和 <relativePath>,用于定位父 POM |
| 是否必需 | 对于聚合构建是必需的 | 对于配置继承是必需的 |
| 能否省略 | 可以省略,意味着不能一键构建所有模块 | 可以省略,意味着子模块不继承任何父配置 |
一句话概括:
用
<modules>在父工程中聚合子模块,用<parent>在子工程中继承父工程的配置。两者结合,才能实现 Maven 多模块项目的统一管理和一键构建。
让我们通过一个具体的项目结构来说明:
my-project/ # 父工程根目录
├── pom.xml # 父 POM (包含 <modules>)
├── common/ # 子模块1
│ └── pom.xml # 子 POM1 (包含 <parent>)
├── service/ # 子模块2
│ └── pom.xml # 子 POM2 (包含 <parent>)
└── web/ # 子模块3└── pom.xml # 子 POM3 (包含 <parent>)1. 父 POM (my-project/pom.xml) - 包含 <modules>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>my-project</artifactId><version>1.0.0-SNAPSHOT</version><packaging>pom</packaging><!-- ✅ 父模块使用 <modules> 来声明它的子模块 --><modules><module>common</module><module>service</module><module>web</module></modules><!-- ... 其他配置如 dependencyManagement, properties 等 ... -->
</project>2. 子 POM (web/pom.xml) - 包含 <parent>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"><modelVersion>4.0.0</modelVersion><!-- ✅ 子模块使用 <parent> 来声明它的父模块 --><parent><groupId>com.example</groupId><artifactId>my-project</artifactId><version>1.0.0-SNAPSHOT</version><relativePath>../pom.xml</relativePath> <!-- 指向父 POM --></parent><artifactId>web</artifactId><!-- version 继承自父模块 --><!-- ... 子模块自己的 dependencies, build 等配置 ... -->
</project>如果缺少其中一个会怎样?
| 情况 | 结果 |
|---|---|
有 <modules> 但没有 <parent> | 只有聚合,没有继承。 • 你可以在父目录用 mvn install 一键构建所有模块。• 但是,所有子模块无法继承父 POM 中定义的 dependencyManagement、properties、pluginManagement 等配置。每个子模块都需要自己重复定义这些公共配置,失去了统一管理的意义。 |
有 <parent> 但没有 <modules> | 只有继承,没有聚合。 • 所有子模块都可以继承父 POM 的配置,实现了统一管理。 • 但是,你无法在父目录运行一条命令来构建所有模块。你必须手动进入每个子模块目录,依次执行 mvn install。这在模块数量多时非常低效。 |
| 两者都有 ✅ | 完美的聚合与继承。 • 在父目录运行 mvn clean install,Maven 会:1. 通过 <modules> 知道有哪些子模块。2. 通过 <parent> (在子 POM 中) 确保每个子模块都继承了公共配置。3. 自动分析依赖关系并按正确顺序构建所有模块。 • 这是开发大型项目的最佳实践。 |
结论
<parent>和<modules>不仅可以同时出现,而且是构建一个功能完整的 Maven 多模块项目所必需的。- 它们是互补的关系,分别解决了配置继承和项目聚合这两个核心问题。
<modules>在父 POM 中定义,负责“组织”。<parent>在每个子 POM 中定义,负责“传承”。- 两者缺一不可。只有当它们“同时出现”时,Maven 的父子工程模式才能发挥出最大的威力,实现高效、统一的项目管理。
