Maven多模块项目拆分实战:从50万行单体到独立模块的演进
Ma ven多模块项目拆分实战:从50万行单体到独立模块的演进
适用环境:Ma ven 3.9.x、Spring Boot 3.2.x、Ja va 17、MyBatis-Plus 3.5.x
一个80万行的单体项目,20人的团队,编译要等25分钟——这场景是不是听着就头大?去年某个电商平台的后端重构,正好就撞上了这个坎。项目从2019年起步,最初只有用户和商品两个模块,后来订单、支付、营销、库存陆续往里塞,到2023年底,代码量膨胀到了80万行,全部挤在一个Ma ven工程里。
团队从最初的3人增长到20人,可问题也跟着来了:编译时间失控,改一行代码要等半天;代码冲突频发,每天早上的合并冲突能占掉1小时;部署粒度太粗,改个支付小功能就得重新部署整个系统;新人上手更是噩梦,clone下来IDEA索引就要5分钟,找个类得在几百个包里翻。经过两周调研,团队决定用Ma ven多模块方案重构,目标是把单体拆成按业务域划分的独立模块,支持独立编译和部署,为后续微服务化铺路。下面就把整个拆分过程梳理出来,重点讲清楚模块划分思路、依赖关系处理和编译优化。
多模块能解决什么问题
多模块的本质,说白了就是把一个大工程按职责拆成多个小工程,通过Ma ven的父子POM机制统一管理。它的核心价值在于:编译不再全局等待,改动只影响局部;代码冲突范围缩小,每人只碰自己负责的模块;部署也能按需进行,风险可控。

第一章:模块拆分原则与方案选型
1.1 三条核心指导原则
拆分前必须想清楚边界,否则拆出来的模块会比单体更乱。我们遵循三条原则:
高内聚低耦合:模块内部高度相关,模块之间依赖最小化,依赖方向单向;
单一职责:每个模块只做一件事,职责边界清晰,避免"大杂烩"模块;
稳定依赖原则:不稳定模块依赖稳定模块,业务模块依赖基础模块,底层不依赖上层。
这三条原则不是理论口号,而是后续每个模块划分决策的判断标准。遇到拿不准的拆法时,回到这三条原则上衡量。
1.2 两种主流拆分方式对比
业界有两种主流拆分方式,适用场景不同。
方式一:按业务域拆分(推荐)
按业务功能划分模块,每个业务域包含自己的 api、service、dao 三层。适合业务边界清晰的项目。
parent/
├── common/ # 公共模块
│ ├── common-util/ # 工具类
│ ├── common-constant/ # 常量定义
│ └── common-exception/# 异常处理
├── user/ # 用户模块
│ ├── user-api/ # API 接口
│ ├── user-service/ # 业务逻辑
│ └── user-dao/ # 数据访问
├── order/ # 订单模块
│ ├── order-api/
│ ├── order-service/
│ └── order-dao/
└── web/ # Web 层
├── admin-web/ # 管理后台
└── app-web/ # C 端应用
方式二:按技术层次拆分
按 api、service、dao 层划分,每层包含所有业务模块。适合技术栈统一、强调层次分明的项目。
parent/
├── api-gateway/ # API 网关
├── service-layer/ # 服务层
│ ├── user-service/
│ ├── order-service/
│ └── product-service/
├── dao-layer/ # 持久层
│ ├── user-dao/
│ ├── order-dao/
│ └── product-dao/
└── web-layer/ # Web 层
├── controller/
└── dto/
方案对比:
| 维度 | 按业务域拆分 | 按技术层拆分 |
|---|---|---|
| 业务边界 | 清晰,一个业务一个模块 | 模糊,业务分散在各层 |
| 团队分工 | 按业务域分团队 | 按技术层分团队 |
| 微服务演进 | 容易,业务模块可直接抽出 | 困难,需重新拆分 |
| 跨层复用 | 业务内部复用方便 | 跨业务复用方便 |
我们的项目最终选了按业务域拆分,因为团队是按业务线组织的,且后续有微服务化计划。
第二章:电商平台拆分实战
2.1 项目背景与目标
原始状态:
- 单体项目,80 万行代码
- 包含用户、商品、订单、支付、营销、库存等业务
- 20 人开发团队,按业务线分组
- 编译时间 25 分钟,部署频率每周 1 次
拆分目标:
- 按业务域拆成 6 个中心模块
- 支持独立编译和按需部署
- 编译时间控制在 5 分钟内
- 为后续微服务化打基础
2.2 最终模块结构
拆分后的项目结构如下,每个业务中心包含 api、service、dao 三层,公共能力下沉到 platform-common:
ecommerce-platform/ # 父项目
├── pom.xml
├── docs/ # 文档目录
├── scripts/ # 脚本目录
│
├── platform-common/ # 平台公共模块
│ ├── pom.xml
│ └── src/main/ja va/com/company/common/
│ ├── util/ # 工具类
│ ├── constant/ # 常量定义
│ ├── exception/ # 异常处理
│ └── response/ # 统一响应
│
├── user-center/ # 用户中心
│ ├── pom.xml
│ ├── user-api/ # 用户 API(接口 DTO)
│ ├── user-service/ # 用户服务实现
│ └── user-dao/ # 用户数据访问
│
├── product-center/ # 商品中心
│ ├── product-api/
│ ├── product-service/
│ └── product-dao/
│
├── order-center/ # 订单中心
│ ├── order-api/
│ ├── order-service/
│ └── order-dao/
│
├── pay-center/ # 支付中心
│ ├── pay-api/
│ ├── pay-service/
│ └── pay-dao/
│
└── web-gateway/ # Web 网关
├── pom.xml
└── src/main/ja va/com/company/web/
├── controller/ # 控制器
├── config/ # 配置类
└── filter/ # 过滤器
2.3 父 POM 配置
父 POM 负责统一管理版本号和公共依赖,子模块只声明依赖不写版本号。这样改版本时只改父 POM 一处:
4.0.0
com.company
ecommerce-platform
1.0.0-SNAPSHOT
pom
platform-common
user-center
product-center
order-center
pay-center
web-gateway
17
17
17
UTF-8
3.2.0
8.2.0
3.5.4
32.1.3-jre
1.18.30
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
com.company
platform-common
${project.version}
com.company
user-api
${project.version}
org.springframework.boot
spring-boot-ma ven-plugin
${spring-boot.version}
org.apache.ma ven.plugins
ma ven-compiler-plugin
3.11.0
2.4 子模块 POM 示例
子模块通过 继承父 POM,依赖列表只写 groupId 和 artifactId,版本号由父 POM 管理:
com.company
user-center
1.0.0-SNAPSHOT
user-service
jar
com.company
user-api
com.company
platform-common
org.springframework.boot
spring-boot-starter
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
org.springframework.boot
spring-boot-starter-test
test
第三章:循环依赖破解
3.1 循环依赖是怎么产生的
拆分过程中最容易踩的坑就是循环依赖。下面是我们在拆分用户和订单模块时遇到的真实问题:
// 错误示范:user-service 直接依赖 order-service
public class UserService {
@Autowired
private OrderService orderService; // 循环依赖开始
public User getUser(Long userId) {
List orders = orderService.getUserOrders(userId); // 构建用户视图时需要订单信息
}
}
// order-service 又依赖 user-service
public class OrderService {
@Autowired
private UserService userService; // 形成循环
public List getUserOrders(Long userId) {
User user = userService.getUser(userId); // 查订单时需要校验用户
}
}
这种依赖关系会导致 Ma ven 无法决定先编译哪个模块,构建直接失败。
3.2 解决方案:抽取公共 API 模块
核心思路是把"接口定义"和"实现"分离。api 模块只放接口和 DTO,不依赖任何 service 模块;service 模块依赖 api 模块实现接口。这样依赖方向永远是单向的:
方案:抽取公共 API 模块
user-api/
├── UserService.ja va # 只定义接口
├── UserDTO.ja va # 用户 DTO
└── OrderDTO.ja va # 订单 DTO 也放这里(被 user 引用时)
order-api/
├── OrderService.ja va # 订单接口
└── OrderDTO.ja va
user-service/
└── UserServiceImpl.ja va # 实现类,只依赖 user-api
order-service/
└── OrderServiceImpl.ja va # 依赖 order-api 和 user-api
依赖关系:user-service → user-api
order-service → order-api
↘ user-api 单向依赖,无循环
关键点:DTO 的归属要明确。如果一个 DTO 被多个模块使用,放到更底层的 common 模块或被依赖方的 api 模块里。我们的原则是"DTO 跟着接口走"。
3.3 版本统一管理
多模块项目最大的坑是版本不一致。子模块引用其他模块时如果不统一版本,会出现编译通过但运行时 NoSuchMethodError 的问题。
在父 POM 中统一定义版本号,子模块引用时不写版本号:
3.2.0
8.2.0
1.0.0-SNAPSHOT
org.springframework.boot
spring-boot-starter
com.company
platform-common
3.4 SNAPSHOT 版本的使用建议
开发阶段用 SNAPSHOT 版本,方便随时发布更新;生产环境必须用 RELEASE 版本,保证可追溯:
# 开发阶段使用 SNAPSHOT,依赖方总能拉到最新代码
# 版本号:1.0.0-SNAPSHOT
# CI/CD 流水线强制更新 SNAPSHOT 依赖
mvn clean install -U
# 生产环境使用 RELEASE 版本
# 版本号:1.0.0(无 SNAPSHOT 后缀)
第四章:编译与部署优化
4.1 并行编译
多模块项目最大的收益是支持并行编译。Ma ven 的 -T 参数可以指定并行线程数:
# 使用 4 个线程并行编译
mvn clean install -T 4
# 根据 CPU 核心数自动调整(每个核心一个线程)
mvn clean install -T 1C
实测效果对比(80 万行代码,20 个模块):
| 编译方式 | 耗时 | 提升幅度 |
|---|---|---|
| 串行编译 | 25 分钟 | 基准 |
| 4 线程并行 | 8 分钟 | 下降 68% |
| 8 线程并行 | 6 分钟 | 下降 76% |
4.2 增量编译
日常开发不需要每次都全量编译,只编译改动的模块即可:
# 只编译 user-service 模块及其依赖的模块
mvn install -pl :user-service -am
# 参数说明:
# -pl 指定要构建的模块
# -am 同时构建依赖的模块(also make)
这样改一个模块只需编译它和它的上游依赖,从 25 分钟降到 2 分钟以内。
4.3 跳过不必要的检查
本地开发时跳过测试和代码检查,加快构建速度:
# 跳过测试执行(但仍编译测试代码)
mvn clean package -DskipTests
# 彻底跳过测试编译和执行
mvn clean package -Dma ven.test.skip=true
# 同时跳过代码检查(PMD、CheckStyle)
mvn clean package -DskipTests -Dpmd.skip=true -Dcheckstyle.skip=true
第五章:最佳实践与避坑总结
5.1 模块划分检查清单
判断一个模块划分是否合理,对照以下清单:
好的模块特征:
- 职责单一,功能聚焦
- 有清晰的边界,对外暴露的接口稳定
- 可以独立编译和测试
- 被其他模块依赖但不反向依赖
- 有明确的复用价值
不好的模块特征:
- 什么都往里塞(大杂烩模块)
- 与其他模块循环依赖
- 无法独立运行或测试
- 职责模糊,边界不清
5.2 命名规范
统一的命名规范能让团队快速理解模块职责:
user-api
user-service
user-dao
platform-common
module1
test-module
5.3 依赖管理原则
依赖方向必须单向,从上到下:
依赖方向:Web 层 → Service 层 → DAO 层 → Common 工具层(最底层)
依赖规则:
- 上层可以依赖下层
- 同层可以依赖(通过 api 模块)
- 下层不能依赖上层
- 避免跨层依赖(除非必要)
5.4 拆分前后效果对比
改造前后效果对比(基于实际项目数据):
| 指标 | 拆分前 | 拆分后 | 改善 |
|---|---|---|---|
| 全量编译时间 | 25 分钟 | 6 分钟 | 下降 76% |
| 增量编译时间 | 25 分钟 | 2 分钟 | 下降 92% |
| 代码冲突频率 | 每天 5-8 次 | 每天 1-2 次 | 下降 80% |
| 新人上手时间 | 3 天 | 1 天 | 下降 67% |
| 部署粒度 | 整体部署 | 按模块部署 | 灵活 |
一键构建脚本
下面是经过生产验证的多模块构建脚本,支持按环境构建和选择性跳过测试:
#!/bin/bash
# build-all.sh - 多模块项目一键构建脚本
# 用法:./build-all.sh [dev|test|prod] [true|false]
set -e # 遇到错误立即退出
PROFILE=${1:-dev}
SKIP_TESTS=${2:-true}
echo "开始构建,环境:$PROFILE,跳过测试:$SKIP_TESTS"
# 1. 清理旧构建
mvn clean
# 2. 安装父 POM(其他模块依赖它)
mvn install -pl . -am -P$PROFILE
# 3. 构建公共模块(其他业务模块依赖它)
mvn install -pl platform-common -am -P$PROFILE -DskipTests=$SKIP_TESTS
# 4. 构建各业务中心
for center in user-center product-center order-center pay-center; do
echo "构建 $center..."
mvn install -pl $center -am -P$PROFILE -DskipTests=$SKIP_TESTS
done
# 5. 构建 Web 网关(最终产物)
mvn package -pl web-gateway -am -P$PROFILE -DskipTests=$SKIP_TESTS
# 6. 输出构建统计
echo "构建完成"
find . -name "*.jar" -type f | wc -l | xargs echo "生成 jar 包数:"
du -sh target/ 2>/dev/null | cut -f1 | xargs echo "构建产物大小:"
使用方法:
# 开发环境构建(跳过测试)
./build-all.sh dev true
# 测试环境构建(执行测试)
./build-all.sh test false
总结
本文从一个 80 万行单体的拆分实践出发,介绍了 Ma ven 多模块项目的完整搭建方法。核心要点:
| 问题 | 解决方案 |
|---|---|
| 模块边界怎么划? | 按业务域划分,每个业务一个中心模块 |
| 循环依赖怎么办? | 抽取 api 模块,接口与实现分离 |
| 版本怎么管? | 父 POM 的 统一声明 |
| 编译太慢? | 用 -T 并行编译,或 -pl 增量编译 |
关键原则:
- 拆分前先画模块依赖图,确保依赖方向单向
- 公共代码下沉到 common 模块,不要散落在业务模块
- api 模块只放接口和 DTO,不放实现,方便后续微服务化
- 日常开发用
-pl xxx -am只编译改动模块