本节目标:给出「单体还是多模块」的可操作判据——什么时候该忍住不拆,出现哪些信号才该动手,以及三种拆分粒度各自要付出什么代价。
适用版本:Spring Boot 4.1.x(Java 21)
1.1 何时该拆模块
入门卷第 18 章用「图书借阅管理服务」把 Book、Member、Loan 三个实体、八个接口和一套全局异常处理串成了单个可交付应用。它当时是一个 Maven 单模块工程,src/main/java 下按 domain、repository、service、dto、web 分包,mvn package 出一条 jar 就能跑。这个结构在第一个版本完全够用。
实战卷从本节开始把它推向生产:本章先拆出多模块骨架,后面十七章都在这个骨架上做增强。但要先回答一个更靠前的问题——到底该不该拆? 多模块不是「更专业」的默认选项,它是一个有明确成本的结构决策。本节不讲「怎么拆」,只讲「什么时候别拆」和「什么时候必须拆」。
1.1.1 为什么「拆模块」是个决策而不是默认
先说清楚拆模块到底改变了什么。它不只是把 package 变成 module,而是引入了三条新的硬约束:
- 编译期隔离:模块 A 看不到模块 B 的内部类,跨模块调用必须经过公开 API。
- 依赖显式化:每个模块有自己的
pom.xml,用到的依赖必须自己声明,不能靠「碰巧在同一个 classpath 上」。 - 独立构建产物:每个模块产出自己的 jar,可以单独测试、单独发布、单独复用。
这三条约束是双刃剑。它们带来的是「边界不会腐化」,付出的是「改一个类可能要动三个 pom.xml」。判断该不该拆,本质就是判断这些约束带来的收益,是否已经超过它的维护成本。
一个反直觉的事实:绝大多数团队拆模块的时机都太早了。单模块的「混乱」是可见的(一个 service 包里 80 个类),而多模块的「僵化」是隐性的(每加一个跨模块功能都要开会决定放哪个模块)。人们倾向于解决看得见的问题,于是过早地引入看不见的问题。
1.1.2 四个决策维度
不要凭感觉判断,用四个可观察的维度来量。
| 维度 | 支撑拆模块的情形 | 支撑保持单体的情形 |
|---|---|---|
| 团队规模 | 多个小组各自负责一块,需要独立改代码 | 一个小组(≤ 5 人)从头做到尾 |
| 发布节奏 | 不同部分要按不同节奏独立发版 | 所有功能一起打包、一起上线 |
| 复用需求 | 核心领域逻辑要被第二个应用复用 | 只有一个应用会用到这些代码 |
| 编译耗时 | 单模块构建已拖慢到影响日常开发 | 全量构建还在几十秒内,可接受 |
四个维度要一起看,任何一个单独成立都不足以支撑拆分。比如「编译耗时 3 分钟」看着难受,但如果只有一个小组、没有任何复用需求,把 mvn 拆成三份只会让第一次全量构建更慢(多了模块间协调开销),日常增量编译反而变快有限——收益不足以抵消新增的 pom.xml 维护成本。
编译耗时这一项尤其容易被误判。 单模块编译慢,第一反应往往是「拆模块并行编译就好了」。但 Maven 的并行构建(-T)在单模块内就能用,而多模块的并行受模块间依赖顺序限制,加速比通常远低于预期。真正拖慢编译的往往是注解处理器、测试数量或依赖解析,拆模块治不了这些病。第二节会专门讲构建加速的正确手段。
「复用需求」这一项则可以看得很具体。假设图书借阅的核心校验逻辑现在长这样:
package com.example.library.service;
public class LoanPolicy {
public boolean canBorrow(Book book, Member member) {
return book.getAvailableCopies() > 0
&& member.isActive()
&& !hasOpenLoan(member, book);
}
}
如果批处理任务也想用这套规则,在单模块里只能把它复制一份——两份 LoanPolicy 从此各自演化,改了一处忘了另一处,就是线上事故的种子。此时「复用需求」这一维度亮红灯,说明该把 LoanPolicy 连同它依赖的实体一起抽进 book-loan-core,让两个应用都引同一个 jar。判断复用的标准是「真的已经有第二个消费者」,而不是「将来可能会有」。
1.1.3 「先别拆」的判断标准
下面任何一条成立,就应该忍住不拆。这不是保守,是避免过早付出成本。
- 代码量还没到瓶颈:整个
src/main/java下不足约 100 个类,或单个包下不足约 30 个类。这个规模下,IDE 的导航和搜索完全够用,模块边界带来的收益微乎其微。 - 只有一个部署单元:如果所有代码最终都打进同一个可执行 jar、一起上线,那么「独立构建产物」这条收益根本不成立。
- 团队没有分工边界:当所有人都在改所有包时,模块边界只会变成「每次提交前先解决冲突」的负担,而不会自然形成职责划分。
- 没有第二个消费者:如果核心逻辑只会被当前应用调用,抽成独立库模块只是把类从一个目录搬到另一个目录。
- 边界还没稳定:业务早期,领域边界每周都在变。此时按业务切模块,等于把「还在动的边界」固化进
pom.xml,改起来比改包名贵得多。
一句话总结:单模块的问题,用包结构和纪律就能解决大部分;多模块解决的是「包结构管不住的边界」,而边界在稳定之前不该被固化。
1.1.4 「该拆了」的信号
反过来,出现下面这些信号,就说明单模块已经开始漏气,是时候动手了。
| 信号 | 具体表现 | 说明什么 |
|---|---|---|
| 构建变慢到影响节奏 | 全量构建稳定超过 3~5 分钟,且原因就是代码量 | 编译耦合已经形成 |
| 复用变成复制粘贴 | 同一段领域逻辑在第二个项目里被复制 | 缺一个可复用库模块 |
| 变更互相牵连 | 改订单相关代码会牵动库存相关代码,且两者由不同人负责 | 缺业务边界 |
| 依赖方向失控 | 无法从代码看出谁依赖谁,出现「工具类被所有人引用」 | 缺编译期约束 |
| 发布互相阻塞 | A 组的功能因为 B 组还没测完而无法上线 | 缺独立发布单元 |
这些信号有一个共同特征:它们都是「单模块已经产生的实际痛感」,而不是「未来可能的风险」。 决策依据要基于已发生的痛,不要基于对未来的想象。等到痛感真实出现再拆,成本可控;提前拆,是在为还没发生的问题付费。
1.1.5 三种拆分粒度
确定要拆之后,接下来选粒度。三种常见拆法各有明确的适用场景和代价。
粒度一:按层拆。 每个技术层一个模块,如 book-loan-web、book-loan-service、book-loan-repository。
book-loan/
├── book-loan-web/ # Controller、DTO、全局异常处理
├── book-loan-service/ # 业务规则、事务边界
├── book-loan-repository/ # 实体、Repository 接口
└── pom.xml # 聚合父 POM
| 优点 | 代价 |
|---|---|
| 边界清晰,几乎不用讨论 | 加一个功能要同时改三个模块,跨层改动频繁 |
| 强制单向依赖,天然防循环 | 无法按业务独立发布,一个功能仍要三模块一起发 |
| 上手成本低 | 领域概念被技术层切碎,找「借书」相关代码要跨三个模块 |
按层拆适合「团队按技术分工」或「需要强制分层纪律」的场景,但它几乎不带来复用收益,发布也还是整体的。多数业务系统用它,主要图的是「防止 Controller 直接依赖 Repository」这类纪律能落到编译期。
粒度二:按业务拆。 每个领域子域一个模块,如 book-loan-book、book-loan-member、book-loan-loan。
book-loan/
├── book-loan-book/ # 图书子域:Book 实体、图书用例
├── book-loan-member/ # 会员子域:Member 实体、会员用例
├── book-loan-loan/ # 借阅子域:Loan 实体、借还用例
├── book-loan-web/ # 只做路由装配的入口
└── pom.xml
| 优点 | 代价 |
|---|---|
| 按业务边界组织,与团队分工对齐 | 领域边界必须已经稳定,否则模块会频繁重组 |
| 子域可独立测试、独立演进 | 跨子域调用需要精心设计公开 API,易产生「循环依赖」 |
| 未来可平滑拆成服务 | 借还操作天然跨 book 与 loan,边界划不干净就要妥协 |
按业务拆适合「领域边界已清晰、且团队按业务线分工」的场景。 它最贴近微服务的前置形态,但也最考验边界设计——借书这个动作同时要动 Book 的库存和 Loan 的记录,两个子域之间的依赖方向必须先想清楚。
粒度三:按可复用库拆。 把与业务无关或可跨项目复用的能力抽成库,如 book-loan-common、book-loan-client。
book-loan/
├── book-loan-common/ # 通用工具、错误码、基础 DTO
├── book-loan-client/ # 供其他系统调用的 Feign/HTTP 客户端契约
├── book-loan-app/ # 业务应用(单模块,内部再分包)
└── pom.xml
| 优点 | 代价 |
|---|---|
| 复用成本最低,第二个项目直接引依赖 | 库一旦发布就要考虑向后兼容,改 API 变贵 |
| 库可独立版本化、独立发布 | 过度抽取会让「common」变成新的垃圾场 |
| 主应用内部仍可保持单模块 | 若 common 抽得太细,会出现「为两个类建一个模块」 |
按可复用库拆是收益最确定、风险最低的一种,因为它不依赖领域边界是否稳定。判断标准很直接:同一段代码真的被两个以上项目用了,才抽成库;只有一个消费者,就不要抽。
1.1.6 三种粒度可以组合
真实项目里三者并不互斥。本章给「图书借阅管理服务」定的骨架就是组合式的:
book-loan/
├── pom.xml # 聚合 + 依赖管理父 POM
├── book-loan-api/ # 对外契约:DTO、错误码、接口定义(可复用库)
├── book-loan-core/ # 领域与业务:实体、Repository、Service(可复用库)
└── book-loan-web/ # 应用入口:Controller、配置、启动类
book-loan-api 与 book-loan-core 属于「按可复用库拆」——它们未来可能被批处理任务或管理后台复用;book-loan-web 是唯一有 main 方法、唯一能独立启动的应用模块。依赖方向被固定为一条直线:
book-loan-web ──依赖──▶ book-loan-core ──依赖──▶ book-loan-api
这个骨架刻意没有按业务再细分。因为对一个只有三个实体的服务,按业务拆成三个模块属于过度设计——边界还在稳定期,先按「可复用库 + 应用」切两刀,等业务长到信号出现时再细分。这正是 1.1.3 那条「边界还没稳定就先别按业务拆」的落地。
1.1.7 用数字量化四个维度
四个维度不要靠印象,先量出来再判断。三条命令覆盖最关键的三个数。
# 1. 全量构建耗时:连跑三次取稳定值,首次含拉依赖会虚高
mvn -q clean verify -Dmaven.test.skip=true
# 2. 规模:src/main/java 下的类数与单个最大包的类数
find src/main/java -name '*.java' | wc -l
# 3. 并行构建加速比:-T 1C 表示每核一个线程
mvn -q clean package -T 1C
第 1 条命令给出的是「单模块痛感」的直接证据——只有它稳定超过 3~5 分钟,编译耗时这一维度才算真的亮红灯。第 2 条用类数回答「是否到了规模瓶颈」。第 3 条最容易被忽略:如果 -T 1C 已经能把全量构建压到可接受范围,那么拆模块对构建速度的边际收益就很有限,此时若还要拆,动机应该是复用或发布解耦,而不是「为了更快」。
把单模块与拆分后的量级放在一起看,能直观看到取舍:
| 指标 | 单模块(示例量级) | 三模块(示例量级) | 说明 |
|---|---|---|---|
| 全量构建 | 约 2~3 分钟 | 约 1.5~2 分钟 | 拆模块只小幅提速,且受模块依赖顺序限制 |
| 增量编译 | 快 | 略慢 | 跨模块改动要重建被依赖模块 |
单个 pom.xml 改动 | 1 处 | 2~4 处 | 新增依赖要判断放父还是子 |
| 复用能力 | 无 | 核心模块可直接引 | 这是拆模块最确定的收益 |
上表数字是示例量级,用于说明取舍关系,不是本机实测结果。真实数字必须用上面三条命令在你的项目上自己测。
判断方法很直接:把「复用能力」这一列当作拆分的核心理由,「构建耗时」这一列当作加分项。 如果复用那一列是「无」,而只有构建耗时略有改善,那么这次拆分的性价比大概率不划算。
1.1.8 决策清单
把本节浓缩成一张可以在评审会上直接用的表:
| 问题 | 回答「是」→ 倾向 | 说明 |
|---|---|---|
| 有第二个应用要复用这段逻辑吗? | 拆出库模块 | 复用收益最确定 |
| 有多个小组要独立改代码、独立发版吗? | 拆出业务模块 | 否则边界只是负担 |
| 分层纪律光靠约定守不住吗? | 按层拆 | 编译期强制最有效 |
| 编译慢是因为代码量本身吗? | 先优化构建再考虑拆 | 拆模块未必治编译慢 |
| 领域边界稳定超过半年了吗? | 可考虑按业务拆 | 未稳定就拆会反复重组 |
| 以上都「否」吗? | 保持单模块 | 用包结构和纪律先顶着 |
这张表的用法是从上往下问,第一个「是」就给出了方向;如果全部落到「否」,结论就是先别拆。
拿图书借阅服务走一遍:它会被批处理任务(每日生成逾期提醒)和管理后台两个应用消费,所以第一问为「是」——值得拆出可复用库,于是有了 book-loan-api 与 book-loan-core。但它只有一个小组维护、一次上线,所以第二问为「否」——不值得按业务再拆。最终结论就是本章骨架:两刀切成三个模块,web 只是薄薄的装配层。
这个例子的意义在于:同一个项目,不同维度会给出不同答案,决策是把它们调和成一个够用的结构,而不是让所有维度都「最优」。 如果强行追求「按业务也拆」,得到的会是一个六个模块、每个模块只有几个类的工程,维护成本远超收益。
1.1.9 常见误区
三个几乎每个团队都会踩的坑:
误区一:为了「架构好看」而拆。 把多模块当作「成熟团队」的标志,在没有实际痛感时提前拆,结果引入大量跨模块样板代码,开发效率反而下降。架构的唯一评价标准是「是否解决了真实问题」。
误区二:把多模块当微服务用。 拆了模块就以为可以独立部署,但所有模块仍打进同一个 jar。多模块和微服务是两件事:前者是编译期的边界,后者是运行时的边界。混为一谈会导致「以为解耦了,其实只是换了目录」。
误区三:common 模块无限制膨胀。 一旦有了 common,所有人都往里塞东西,它很快变成「所有模块都依赖、改一次全员重编译」的新单点。约束是:common 只放真正跨模块共享的稳定契约,放不下的说明它该有更具体的归属。
小结
- 拆模块引入编译期隔离、依赖显式化、独立产物三条硬约束,是双刃剑,判断标准是收益是否超过
pom.xml维护成本。 - 用团队规模、发布节奏、复用需求、编译耗时四个维度一起量,单一项成立不足以支撑拆分。
- 「先别拆」的红线:代码量未到瓶颈、只有一个部署单元、无分工边界、无第二消费者、边界未稳定。
- 「该拆了」的信号都来自已发生的痛感,而不是对未来的想象:构建拖慢、复制粘贴、变更牵连、依赖失控、发布阻塞。
- 三种粒度各有代价:按层拆边界清晰但改动频繁,按业务拆贴近微服务但依赖边界稳定,按可复用库拆收益最确定。
- 本章骨架组合了「可复用库 + 应用」两刀:
book-loan-api/book-loan-core/book-loan-web,依赖是一条直线,刻意不按业务细分。 - 最常见的三个误区:为好看而拆、把多模块当微服务、common 模块无限制膨胀。
结构定了,下一步是把骨架变成能构建的工程。1.2 会写出父 POM、dependencyManagement 与子模块的依赖声明,让这套结构真正跑起来。
阅读导航:上一节:目录与学习路径 · 下一节:1.2 父子 POM 与依赖管理 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。