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

    • 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章 仓库管理深入

    • 仓库组与路由

继承与聚合组合实践

本章整合入门教程中分别介绍的"继承"与"聚合"两个概念,讲解在真实项目中如何让同一个父 POM 同时承担两种角色。理解这种组合模式,是阅读开源项目(如 Spring Boot Starter)和企业级多模块项目 POM 结构的必备能力。


核心机制

入门教程已经分别讲过:

  • 继承:子模块通过 <parent> 引用父 POM,复用其依赖管理、插件配置、属性定义等
  • 聚合:父 POM 通过 <modules> 列出子模块,实现一键构建整个项目

在真实项目中,这两个机制几乎总是同时出现、由同一个 POM 承担。这个 POM 既是子模块的 <parent>(继承模板),又是 <modules> 的容器(聚合根)。这种组合不是巧合,而是 Maven 多模块项目的标准设计模式。

父 POM 的双重角色

角色机制标签作用范围对子模块的要求
继承模板<parent>(子模块中声明)版本管理、依赖管理、插件管理、属性子模块必须声明 <parent>
聚合根<modules>(父 POM 中声明)Reactor 构建排序、一键多模块构建子模块无需感知自己被聚合

关键洞察:聚合是"父看子"(父 POM 知道有哪些子模块),继承是"子看父"(子 POM 知道继承自哪个父)。两个方向的引用是独立的——技术上,你可以让一个 POM 只聚合不继承,或只继承不聚合。但在实践中,统一由根 POM 承担两种角色,能最大程度减少配置冗余和版本漂移。

relativePath 的作用

子模块的 <parent> 标签中有一个容易被忽略的元素:<relativePath>。它告诉 Maven:在当前文件系统的哪个相对路径下,可以找到父 POM 文件。

<parent>
    <groupId>com.feixiang</groupId>
    <artifactId>feixiang-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
</parent>

relativePath 的默认值是 ../pom.xml。这意味着:如果子模块位于父 POM 的子目录中(如 feixiang-parent/feixiang-service/),且父 POM 文件就在上级目录(feixiang-parent/pom.xml),那么可以省略 <relativePath>。但如果你的目录结构不是标准的"父目录包含子目录"模式(如子模块和父 POM 平级,或嵌套更深),就必须显式指定 relativePath。

relativePath 的查找优先级:

  1. 先按 relativePath 指向的路径在文件系统中查找父 POM
  2. 如果找不到,再按 <groupId>:<artifactId>:<version> 去本地仓库查找
  3. 如果本地仓库找不到,再去远程仓库查找

这意味着:如果你把 relativePath 写错(如指向了一个不存在的路径),Maven 不会立即报错,而是默默去仓库找一个同 GAV 的父 POM。如果仓库中恰好有一个旧版本,子模块就会继承错误的配置,导致"本地文件改了却不生效"的诡异现象。

实际项目目录布局

企业级 Maven 多模块项目的标准目录布局遵循一个原则:聚合根 POM 位于项目根目录,所有子模块作为子目录存在。这种布局让 relativePath 可以省略,也让版本控制(Git)和 CI/CD 流水线都能以最自然的方式工作。

feixiang-ecommerce/                 ← 项目根目录,也是 Git 仓库根
├── pom.xml                         ← 聚合根 + 继承父 POM(packaging=pom)
├── feixiang-common/                ← 公共工具模块
│   └── pom.xml
├── feixiang-core-api/              ← 接口与 DTO 模块
│   └── pom.xml
├── feixiang-service/               ← 业务逻辑模块
│   └── pom.xml
└── feixiang-web-ui/                ← Web 应用模块
    └── pom.xml

生活类比:家族企业与集团总部

想象飞翔科技从一家小公司成长为集团:

  • 继承:每个子公司(子模块)都认集团总部(父 POM)为母公司,遵守集团的财务制度(依赖管理)、用人标准(插件配置)、品牌规范(属性定义)。子公司自己也可以有独特的业务(独立依赖)。
  • 聚合:集团总部有一份子公司名单(<modules>),年底做集团财报时,按名单逐个审计(Reactor 排序构建)。
  • relativePath:子公司总部大楼的地址。如果所有子公司都在集团总部所在的商业园区里(标准子目录结构),大家默认知道"出电梯左转就是总部"。但如果某个子公司搬到了隔壁城市(非标准目录),就必须在工商登记(pom.xml)上明确写明总部新地址。

图示

上图展示了组合模式的核心结构:父 POM 通过向下的箭头(<modules>)"看"到所有子模块,子模块通过向上的箭头(<parent>)"看"到父 POM。四个子模块同时处于两种关系中,但两种关系的方向相反、职责不同。父 POM 本身不生产代码(packaging = pom),它的全部价值在于统一管理和编排。


完整示例

场景

飞翔科技的电商中台系统已经发展到 4 个子模块。CTO 大翔要求:

  1. 所有模块使用统一的 Spring Boot 版本(3.2.0)
  2. 所有模块的 Java 版本为 17
  3. 一键构建整个项目
  4. 新成员小崔第一天入职就能看懂项目结构

架构师白歌设计了一套"组合父 POM"方案。

操作前的配置/项目状态

项目目录结构:

feixiang-ecommerce/
├── pom.xml                          ← 组合父 POM
├── feixiang-common/
│   └── pom.xml
├── feixiang-core-api/
│   └── pom.xml
├── feixiang-service/
│   └── pom.xml
└── feixiang-web-ui/
    └── pom.xml

feixiang-ecommerce/pom.xml(组合父 POM):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.feixiang</groupId>
    <artifactId>feixiang-ecommerce</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <!-- 聚合:列出所有子模块 -->
    <modules>
        <module>feixiang-common</module>
        <module>feixiang-core-api</module>
        <module>feixiang-service</module>
        <module>feixiang-web-ui</module>
    </modules>

    <!-- 继承模板:统一属性 -->
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-boot.version>3.2.0</spring-boot.version>
    </properties>

    <!-- 继承模板:统一依赖版本 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

feixiang-service/pom.xml(子模块,继承 + 被聚合):

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 继承:指向组合父 POM -->
    <parent>
        <groupId>com.feixiang</groupId>
        <artifactId>feixiang-ecommerce</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <!-- relativePath 默认值 ../pom.xml 正好匹配,可以省略 -->
    </parent>

    <artifactId>feixiang-service</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <!-- 继承来的 dependencyManagement 锁定了版本,这里无需写 version -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 模块间依赖 -->
        <dependency>
            <groupId>com.feixiang</groupId>
            <artifactId>feixiang-core-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
</project>

操作步骤

运维工程师李眉在 CI/CD 流水线中配置了一行命令:

cd feixiang-ecommerce && mvn clean install

新成员小崔第一天入职,在本地执行同样的命令:

cd feixiang-ecommerce
mvn clean install

操作结果

构建输出:

[INFO] ------------------< com.feixiang:feixiang-ecommerce >-----------------
[INFO] Building feixiang-ecommerce 1.0.0-SNAPSHOT                           [1/5]
[INFO] --------------------------------[ pom ]---------------------------------
...
[INFO] -------------------< com.feixiang:feixiang-common >--------------------
[INFO] Building feixiang-common 1.0.0-SNAPSHOT                              [2/5]
...
[INFO] ------------------< com.feixiang:feixiang-core-api >-----------------
[INFO] Building feixiang-core-api 1.0.0-SNAPSHOT                            [3/5]
...
[INFO] ------------------< com.feixiang:feixiang-service >-----------------
[INFO] Building feixiang-service 1.0.0-SNAPSHOT                             [4/5]
...
[INFO] -------------------< com.feixiang:feixiang-web-ui >-------------------
[INFO] Building feixiang-web-ui 1.0.0-SNAPSHOT                                [5/5]
...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for com.feixiang:feixiang-ecommerce:1.0.0-SNAPSHOT:
[INFO] 
[INFO] feixiang-ecommerce ................................. SUCCESS [  0.5 s]
[INFO] feixiang-common ...................................... SUCCESS [  2.1 s]
[INFO] feixiang-core-api .................................... SUCCESS [  1.8 s]
[INFO] feixiang-service ..................................... SUCCESS [  4.5 s]
[INFO] feixiang-web-ui ...................................... SUCCESS [  6.2 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

变化分析:

  • 父 POM feixiang-ecommerce 本身没有代码,但 Reactor 将其作为构建序列的第 1 个节点,用于统一属性解析和插件管理
  • 所有子模块自动继承了 Java 17 配置和 Spring Boot 3.2.0 版本锁定,子模块的 pom.xml 无需重复声明
  • 小崔看到项目结构就知道:根目录的 pom.xml 是"总控",每个子目录是一个模块,模块之间的依赖通过 GAV 声明
  • 李眉的 CI/CD 脚本只需 cd 到项目根目录执行 mvn clean install,无需关心内部有多少模块、按什么顺序构建

易错点与常见问题

误区一:继承和聚合必须由不同的 POM 承担

错误认知:"父 POM 应该只做继承模板,另外再建一个 aggregator POM 来做聚合,这样职责分离更清晰。"

纠正:这种"分离"在 Maven 社区被称为 "聚合器 POM 模式"(Aggregator POM Pattern),它确实存在于某些大型项目中(如 Maven 自身源码),但对于绝大多数企业项目,统一由一个根 POM 承担两种角色是更简洁、更标准的做法。分离模式会增加一个无意义的中间层,让新成员困惑"为什么有两个父 POM",也让版本管理更复杂(两个 POM 的版本需要同步)。只有在子模块数量极多(50+)、且需要按业务线分组聚合时,才考虑分离。

误区二:relativePath 可以随便写,写错了 Maven 会报错

错误认知:"我把 relativePath 写成了 ../../pom.xml,如果路径不对,Maven 构建时会告诉我文件找不到。"

纠正:Maven 对 relativePath 的处理是容错设计:如果 relativePath 指向的文件不存在,Maven 不会报错,而是降级到仓库查找。这意味着:

  • 如果你的本地仓库中恰好有一个同 GAV 的旧版本父 POM,Maven 会默默使用它
  • 你的本地构建可能"成功",但使用的父 POM 是仓库中的旧版本,不是当前目录下的新版本
  • 这会导致"我明明改了父 POM,子模块却不生效"的诡异问题

正确做法:标准目录结构下省略 relativePath,让 Maven 使用默认的 ../pom.xml。非标准结构下,务必确保 relativePath 指向的路径真实存在。

误区三:子模块不声明 <parent> 也能被聚合

错误认知:"父 POM 的 <modules> 里已经列了我的模块,所以我的模块自动继承了父 POM 的配置,不用写 <parent>。"

纠正:聚合和继承是完全独立的机制。父 POM 的 <modules> 只告诉 Reactor"这些模块需要被一起构建",它不会自动让这些模块继承父 POM 的任何配置。如果子模块不声明 <parent>,它就是一个独立的 Maven 项目,不会获得父 POM 中的 dependencyManagement、properties、pluginManagement 等任何配置。在实际项目中,被聚合的子模块几乎总是同时声明 <parent>,否则聚合就失去了统一管理的核心价值。


小结

继承与聚合的组合是 Maven 多模块项目的标准架构模式。同一个父 POM 通过 <modules> 实现聚合(向下看),通过被 <parent> 引用实现继承(向上看)。relativePath 是连接文件系统与仓库查找的桥梁,标准目录结构下应省略以避免路径错误。掌握这种组合模式,才能读懂和设计出清晰、可维护的企业级 Maven 项目结构。

本章与全局的关系:本章整合了继承与聚合的实践。下一章"pluginManagement"将进入插件体系深入,讲解如何在父 POM 中统一管理插件版本和配置——这是继承机制在插件维度上的具体应用。

上一页
Reactor构建顺序
下一页
模块间依赖