文档站点
Skip to content

模块化:多模块 Maven 项目设计 ​

在 PHP 里,common、blog、shop 这种模块,新建一个目录 + 命名空间就算一个模块,全靠自觉约束。Java 里一个模块也可以是一个包(目录),但你会发现 Java 团队更常走另一条路:多模块 Maven 项目 —— 把每个模块做成一个独立的、可单独编译发布的小工程(blog-common、blog-post、blog-shop)。这篇讲清楚为什么,以及怎么设计。

从「目录即模块」到「多模块 Maven」 ​

语言一个模块是什么
PHP目录 + 命名空间(App\Modules\Shop),无硬边界
Java(单模块)一个包(com.example.blog.shop),和 PHP 目录是同一种思维
Java(多模块)一个Maven 子模块(blog-shop/ 目录下有独立 pom.xml)

既然「包」就能当模块,为什么 Java 团队还倾向拆成多模块 Maven?三个关键理由:

  1. 依赖边界由构建工具强制:blog-post 想用 blog-common 的类,必须在 pom 里显式声明依赖,没声明就编译不过 —— 比 PHP「靠自觉遵守命名空间」硬得多
  2. 可独立编译、独立发布:blog-common 可以单独打成一个 jar,被多个应用复用,发版互不影响
  3. 编译隔离 + 团队解耦:A 模块编译失败不影响 B 模块,大团队按模块划分 Git 仓库 / 负责小组都更清晰

TIP

「包」是代码层面的模块化,适合单应用内部;「Maven 子模块」是构建层面的模块化,解决复用、独立发版、硬边界。Java 团队项目一大,往往从包升级到多模块。

多模块项目长什么样 ​

一个博客系统拆成 4 个模块:

blog/
├── pom.xml                 ← 父 POM:统一版本 + 声明子模块
├── blog-common/            ← 通用模块(工具类、统一响应、全局异常)
│   └── pom.xml
├── blog-post/              ← 帖子模块
│   └── pom.xml
├── blog-user/              ← 用户模块
│   └── pom.xml
└── blog-web/               ← 启动模块(唯一能 java -jar 的)
    └── pom.xml

依赖方向(单向,和 包内分层 一致):

blog-web → blog-post / blog-user → blog-common

父 POM:统一管理的枢纽 ​

父 pom.xml 干两件事:声明有哪些子模块 + 集中管理版本。

xml
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>blog</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>          <!-- 父模块打包类型必须是 pom -->

    <!-- 复用 Spring Boot 的统一依赖管理 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
    </parent>

    <!-- 声明子模块 -->
    <modules>
        <module>blog-common</module>
        <module>blog-post</module>
        <module>blog-user</module>
        <module>blog-web</module>
    </modules>

    <!-- 统一管理内部模块版本:子模块依赖时不用写 version -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>blog-common</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>blog-post</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>blog-user</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

关键点:

元素作用
packaging = pom父模块不产出代码,只是「容器」
<modules>列出所有子模块,Maven 按此发现它们
<dependencyManagement>只管版本,子模块声明依赖时省去 version

子模块:各自独立的 pom.xml ​

每个子模块是一个完整小工程,显式声明「我用谁」:

xml
<!-- blog-post/pom.xml -->
<project>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>blog</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>blog-post</artifactId>

    <dependencies>
        <!-- 内部依赖:版本由父 POM 统一管 -->
        <dependency>
            <groupId>com.example</groupId>
            <artifactId>blog-common</artifactId>
        </dependency>
        <!-- 外部依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>
</project>

依赖声明规则

  • 只放自己需要的依赖,别一股脑全塞进 common
  • 内部模块依赖:版本继承父 POM 的 dependencyManagement,别写死 version
  • 谁想用 blog-common,谁就在 pom 里声明 —— 这就是「构建期强制的模块边界」

启动模块:唯一能跑的那个 ​

只有 blog-web 带 spring-boot-maven-plugin,它汇聚所有模块,是可执行应用:

xml
<!-- blog-web/pom.xml -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
bash
# 在父目录构建:Maven 自动按依赖关系排序(common → user/post → web)
mvn clean package

# 运行(产物在启动模块的 target 下)
java -jar blog-web/target/blog-web-1.0.0.jar

TIP

Maven 会自动分析模块间依赖决定编译顺序,不用你手动指定先编谁后编谁。入口类 XxxApplication 也要放在 blog-web 模块里,因为 自动配置的包扫描 基于入口类。

模块命名与边界 ​

命名:项目名-模块名,groupId 相同、artifactId 不同(blog-common、blog-post…)。

依赖方向(和包内分层同理,单向):

web → 业务模块 → common
  • 下层模块不依赖上层(blog-common 绝不能依赖 blog-post)
  • blog-common 只放真正全局的东西:工具类、统一响应、全局异常、常量。它会被所有模块依赖,塞入业务代码就等于没有边界

什么时候用多模块,什么时候单模块就够 ​

场景单模块 + 包多模块 Maven
2~5 个业务模块、一个应用、一个团队✅ 够用可能过度设计
公共模块要被多个应用复用(common)✅ 独立 jar 发布
模块需要独立编译、独立测试、独立发版✅
团队大、代码多、需要硬边界✅

新手建议

别一上来就多模块。单模块 + 包(com.example.blog.post 这种)已经覆盖大部分需求,结构也更简单。等出现「要复用 / 要独立发版」的真实需求再拆多模块。你看到很多开源项目是多模块,那通常是「确实被多个应用引用」或「团队规模决定的」,不是炫技。

常见坑 ​

坑后果
父模块 packaging 忘了写 pom构建报错
非启动模块也加了 spring-boot-maven-plugin打的 jar 跑不起来 / 多个可执行包
子模块各自写死内部依赖 version升级要改一堆 pom,版本漂移
common 塞进业务代码所有模块依赖它,边界名存实亡
入口类放错模块(不在 web)组件扫描不到,启动即失败

小结 ​

问题答案
Java 模块化的核心思路一个业务模块 = 一个包(单模块)或一个 Maven 子模块(多模块)
为什么选多模块 Maven依赖边界构建期强制、可独立编译发布、可跨应用复用
和 PHP 的区别PHP 目录即模块(靠自觉);Java 用 pom 声明依赖(靠编译器 + 构建工具强制)
什么时候拆需要复用 / 独立发版 / 团队规模大时才拆,别为用而用

面向 PHP / Laravel 开发者的 Spring Boot 中文文档