模块化:多模块 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?三个关键理由:
- 依赖边界由构建工具强制:
blog-post想用blog-common的类,必须在 pom 里显式声明依赖,没声明就编译不过 —— 比 PHP「靠自觉遵守命名空间」硬得多 - 可独立编译、独立发布:
blog-common可以单独打成一个 jar,被多个应用复用,发版互不影响 - 编译隔离 + 团队解耦: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 干两件事:声明有哪些子模块 + 集中管理版本。
<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
每个子模块是一个完整小工程,显式声明「我用谁」:
<!-- 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,它汇聚所有模块,是可执行应用:
<!-- blog-web/pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build># 在父目录构建:Maven 自动按依赖关系排序(common → user/post → web)
mvn clean package
# 运行(产物在启动模块的 target 下)
java -jar blog-web/target/blog-web-1.0.0.jarTIP
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 声明依赖(靠编译器 + 构建工具强制) |
| 什么时候拆 | 需要复用 / 独立发版 / 团队规模大时才拆,别为用而用 |