首页 > 教程攻略 > ai教程 >Maven多模块项目拆分实战:从50万行单体到独立模块的演进

Maven多模块项目拆分实战:从50万行单体到独立模块的演进

来源:互联网 时间:2026-06-25 07:25:07

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机制统一管理。它的核心价值在于:编译不再全局等待,改动只影响局部;代码冲突范围缩小,每人只碰自己负责的模块;部署也能按需进行,风险可控。

009-ma ven-multi-module-project-practice_diagram_1.png

第一章:模块拆分原则与方案选型

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 只编译改动模块