在中大型 Java 项目中,单体应用拆分为多模块(multi-module)结构已成为标准实践。它不仅能提升代码复用性,还能明确职责边界、加速编译、支持团队并行开发。
但很多团队在搭建 Spring Boot 多模块项目时,常陷入混乱:模块划分不合理、依赖循环、打包失败、版本冲突……
本文将从 模块划分、依赖管理、构建配置、Root POM 职责 四个维度,分享一套经过生产验证的 Spring Boot 多模块最佳实践。
一、模块划分:按职责而非技术分层
❌ 常见反模式(按技术分层):
project/
├── controller/
├── service/
├── repository/
└── model/
✅ 推荐模式(按业务能力 + 公共组件):
my-order-service/ ← 整个项目根目录(聚合工程)
├── pom.xml
│
├── order-api/ ← 对外暴露的接口契约(DTO + Feign Client)
├── order-domain/ ← 核心领域模型与业务逻辑(充血模型)
├── order-infrastructure/ ← 技术实现(DB、MQ、外部调用)
├── order-application/ ← Spring Boot 启动入口 + 编排层
│
└── common/ ← 跨服务复用的通用组件(非业务专属)
├── common-core/ ← 工具类、异常、枚举、注解
└── common-starter/ ← 自定义 Spring Boot Starter
✨ 模块职责说明:
| 模块 | 职责 | 打包类型 |
|---|---|---|
*-api | 定义对外接口(DTO、Feign Client、OpenAPI 注解) | jar |
*-domain | 领域模型、业务规则、领域服务(纯 Java,无 Spring 依赖) | jar |
*-infrastructure | 技术细节实现(MyBatis、Redis、第三方 SDK 封装) | jar |
*-application | Spring Boot 主启动类、Web 层、事务编排 | jar(可执行) |
common-* | 跨项目复用的基础能力 | jar 或 starter |
💡 核心原则:上层模块依赖下层,下层模块绝不反向依赖上层。
二、如果有多个业务模块怎么办?
✅ 核心原则:按业务能力划分 Maven 模块(module),在模块内部按分层组织 Java 包(package)
也就是说:
每个业务能力 → 一个独立的 Maven 模块(如
order-service) 每个模块内部 → 按 controller / service / dao 分包
📁 推荐结构示例
假设你有 订单(Order) 和 用户(User) 两个核心业务:
my-app/ ← Root POM (packaging=pom)
│
├── order-service/ ← Maven 模块(业务能力:订单)
│ └── src/main/java/com/example/order/
│ ├── controller/ ← OrderController
│ ├── service/ ← OrderService, OrderServiceImpl
│ ├── repository/ ← OrderRepository (或 dao/)
│ └── OrderApplication.java ← Spring Boot 启动类(可选)
│
├── user-service/ ← Maven 模块(业务能力:用户)
│ └── src/main/java/com/example/user/
│ ├── controller/
│ ├── service/
│ ├── repository/
│ └── UserApplication.java
│
└── common/ ← 公共组件模块
└── src/main/java/com/example/common/
├── exception/
├── util/
└── dto/
💡 注意:
order-service和user-service都是 独立的可执行 JAR(各自有@SpringBootApplication),部署时是两个微服务。
❓ 为什么这样做?
| 优势 | 说明 |
|---|---|
| 部署独立 | 订单服务升级不影响用户服务 |
| 团队自治 | 订单团队只维护 order-service 模块 |
| 依赖清晰 | order-service 可依赖 common,但绝不依赖 user-service(避免循环) |
| 技术异构 | 未来订单模块可改用 Kotlin,用户模块保持 Java |
⚠️ 常见误区:把所有业务塞进一个模块
❌ 错误结构(单模块多业务):
monolith-service/
└── src/main/java/com/example/
├── order/
│ ├── controller/
│ └── service/
└── user/
├── controller/
└── service/
问题:
- 无法独立部署(改订单代码要全量发布)
- 依赖容易混乱(
order.service直接调user.service) - 编译慢、JAR 大、启动慢
🌟 微服务 ≠ 必须拆成多个进程,但多模块是迈向微服务的第一步。
三、依赖管理:统一版本,避免冲突
多模块项目最大的挑战之一是 依赖版本一致性。推荐使用 BOM(Bill of Materials)机制。
🔍 补充:spring-boot-dependencies vs spring-boot-starter-parent
很多开发者误以为必须继承 spring-boot-starter-parent,其实二者本质都是“声明式定义”,只是作用范围不同:
| 组件 | 类型 | 作用范围 | 内容 |
|---|---|---|---|
spring-boot-dependencies | BOM(Bill of Materials) | 仅依赖管理 | 声明所有 Spring Boot 官方支持的第三方库版本(如 Jackson、Tomcat、Hibernate),通过 <dependencyManagement> 生效 |
spring-boot-starter-parent | Parent POM | 依赖 + 构建全栈管理 | 继承 spring-boot-dependencies,并额外提供: • Maven 插件版本(<pluginManagement>) • 编译配置(Java 版本、编码) • 资源过滤规则 • 默认打包行为 |
💡 一句话总结:
spring-boot-dependencies是“依赖版本字典”,而spring-boot-starter-parent是“开箱即用的项目模板”。
因此,在多模块项目中,推荐 Root POM 导入 BOM,而非直接继承 starter-parent——这样既能统一依赖,又能避免构建逻辑污染非应用模块。
✅ 在 Root POM 中统一管理依赖版本
<!-- my-order-service/pom.xml -->
<dependencyManagement>
<dependencies>
<!-- 导入 Spring Boot 官方 BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.13</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 导入 Spring Cloud Alibaba BOM -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
子模块中无需指定版本:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 无 version,自动使用 BOM 中的版本 -->
</dependency>
✅ 优势:
- 避免
NoSuchMethodError、ClassNotFoundException等版本冲突- 升级 Spring Boot 只需改 Root POM 一处
四、构建配置:按需启用插件
不是所有模块都需要 Spring Boot 的构建逻辑!
✅ 只在 *-application 模块配置 Spring Boot 插件
<!-- order-application/pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 无需写 version,由 pluginManagement 提供 -->
<configuration>
<mainClass>com.example.order.OrderApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
✅ 其他模块保持轻量
order-api、order-domain、common-core等模块无需任何特殊插件- 它们就是普通的 Java JAR,可被其他服务直接引用
🌟 这样做的好处:
- 编译更快(无 repackage 开销)
- JAR 更小(无嵌入式 Tomcat)
- 可作为普通库发布到 Nexus/Maven Central
五、Root POM 的职责边界:管什么?不管什么?
Root POM(聚合工程)是整个项目的“宪法”,它的核心职责是统一规范、消除重复、保障一致性,但绝不应越俎代庖。
✅ 必须管理的内容
| 类别 | 具体内容 | 说明 |
|---|---|---|
| 依赖版本 | 通过 <dependencyManagement> 导入 BOM | 包括 Spring Boot、Spring Cloud、公司内部组件等 |
| 插件版本 | 通过 <pluginManagement> 声明插件元数据 | 如 maven-compiler-plugin、spring-boot-maven-plugin 等,只声明版本,不启用 |
| 全局属性 | Java 版本、编码、测试覆盖率阈值等 | 例如 <java.version>17</java.version> |
| 模块聚合 | <modules> 列出所有子模块 | 控制构建顺序 |
❌ 不应包含的内容
| 类别 | 原因 |
|---|---|
具体依赖(在 <dependencies> 中) | 会导致所有子模块强制引入,违反“按需依赖”原则 |
启用插件(在 <build><plugins> 中) | 会让非应用模块(如 common)继承不必要的构建逻辑 |
| 应用专属配置 | 如 mainClass、数据库连接等,应下放到具体模块 |
🌰 正确示例(Root POM 片段):
<build> <pluginManagement> <plugins> <!-- 声明版本,但不启用 --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </pluginManagement> </build>
🚫 错误示例:
<!-- Root POM 中直接启用插件(污染所有子模块) --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <!-- 这会让 common 模块也尝试 repackage! --> </plugin> </plugins> </build>
六、避免循环依赖:架构红线
循环依赖是多模块项目的“癌症”。常见场景:
order-service → user-api
user-service → order-api ❌(如果 order-api 依赖了 user-api)
✅ 防御措施:
- API 模块只包含 DTO 和接口,不包含业务逻辑
- 禁止 API 模块依赖其他服务的 API 模块
- 如需关联,通过 ID 引用(如
userId: Long),而非嵌入对象
- 如需关联,通过 ID 引用(如
- 使用 ArchUnit 或 Maven Enforcer Plugin 在 CI 中检测循环依赖
七、结语
一个优秀的多模块项目,不是简单地把代码拆成多个文件夹,而是通过清晰的边界、统一的规范、合理的依赖流向,构建出易于理解、易于维护、易于扩展的系统骨架。
Spring Boot 本身并不强制你如何组织代码,但它提供了足够的灵活性,让我们能结合领域驱动设计(DDD)、分层架构、BOM 管理等理念,打造出真正生产就绪的工程结构。
好的架构,是让新人第一天就能看懂代码流向。