12、多模块项目:多模块项目结构、模块间依赖、统一版本管理、聚合与继承
说实话,我见过太多人把多模块项目搞砸了。
明明是个小项目,硬拆成七八个模块,结果依赖关系乱成一锅粥。或者反过来,一个巨型单体项目,几万行代码塞在一个模块里,每次编译都要喝杯咖啡等它跑完。
多模块项目,说白了就是「分而治之」的思想。但怎么分、怎么治,这里头有门道。
12.1 多模块项目结构:怎么拆才合理?
我个人习惯,先看业务边界。一个模块应该对应一个清晰的业务领域或技术职责。比如一个电商系统,可以拆成:
- common:公共工具类、常量、异常定义
- dal:数据访问层,放 MyBatis Mapper、实体类
- service:业务逻辑层
- web:Controller 层,对外暴露 API
- api:对外提供的 RPC 接口定义
嗯,这里要注意:不是拆得越细越好。模块多了,构建时间反而可能变长,因为 Gradle 要处理更多的配置和依赖解析。
典型的目录结构长这样:
my-project/
├── settings.gradle.kts
├── build.gradle.kts # 根项目构建文件
├── common/
│ ├── build.gradle.kts
│ └── src/
├── dal/
│ ├── build.gradle.kts
│ └── src/
├── service/
│ ├── build.gradle.kts
│ └── src/
└── web/
├── build.gradle.kts
└── src/
根目录的 settings.gradle.kts 里,要声明所有子模块:
rootProject.name = "my-project"
include(
"common",
"dal",
"service",
"web"
)
我在项目中遇到过有人把模块名写成 :common 带冒号的,结果 Gradle 报错半天找不到原因。记住,include 里直接写模块名就行,Gradle 会自动拼接路径。
12.2 模块间依赖:别搞成循环引用
模块之间怎么依赖?很简单,在子模块的 build.gradle.kts 里声明:
// service/build.gradle.kts
dependencies {
implementation(project(":common"))
implementation(project(":dal"))
}
// web/build.gradle.kts
dependencies {
implementation(project(":service"))
}
这里有个铁律:绝对不能出现循环依赖。比如 A 依赖 B,B 又依赖 A。Gradle 会直接报错,而且这种错误在大型项目里特别难排查。
另外,注意 implementation 和 api 的区别:
- implementation:依赖只对当前模块可见,不会传递到下游
- api:依赖会透传给依赖当前模块的其他模块
我建议默认用 implementation,除非你明确需要暴露依赖。这样可以减少不必要的编译开销。
12.3 统一版本管理:别再手动改版本号了
多模块项目最头疼的问题之一:版本号散落在各个模块里。升级一个依赖版本,要改十几个文件,漏一个就出问题。
Gradle 提供了几种统一管理版本的方式,我推荐用 Version Catalog(版本目录),这是 Gradle 7.0+ 官方推荐的做法。
在根目录创建 gradle/libs.versions.toml:
[versions]
spring-boot = "3.2.0"
mybatis = "3.5.16"
lombok = "1.18.30"
[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
mybatis-spring-boot-starter = { module = "org.mybatis.spring.boot:mybatis-spring-boot-starter", version.ref = "mybatis" }
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
然后在子模块里引用:
dependencies {
implementation(libs.spring.boot.starter.web)
implementation(libs.mybatis.spring.boot.starter)
compileOnly(libs.lombok)
}
这样,所有版本号都集中在 libs.versions.toml 里。改一个地方,所有模块同步更新。
libs.versions.toml 里也定义插件版本,比如 Kotlin、Spring Boot 插件。这样整个项目的版本管理就完全统一了。
12.4 聚合与继承:Gradle 的两种复用机制
这两个概念容易混淆,我简单解释一下。
聚合(Aggregation):根项目把子模块聚在一起,统一构建。你执行 ./gradlew build,所有子模块都会构建。这就是聚合。
继承(Inheritance):子模块可以继承根项目的配置。比如所有子模块都用同样的仓库、插件、依赖版本。
在 Gradle 里,继承通过 allprojects 或 subprojects 实现:
// 根项目的 build.gradle.kts
subprojects {
apply(plugin = "java")
apply(plugin = "io.spring.dependency-management")
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
这样,所有子模块自动继承 Java 版本、仓库配置。你不需要在每个子模块里重复写。
但要注意:不要滥用继承。我见过有人把业务逻辑也写在根项目的 subprojects 里,结果子模块之间耦合严重。继承只适合放基础设施配置,比如插件、仓库、编译选项。
12.5 一张图看懂多模块项目
下面这张 SVG 图,展示了多模块项目的核心结构:
从这张图可以清楚看到:根项目负责继承配置(插件、仓库、编译选项),子模块被聚合在一起统一构建,模块之间通过 project() 建立依赖关系,版本号统一由 libs.versions.toml 管理。
12.6 避坑指南
最后,分享几个我踩过的坑:
- 模块命名不要用驼峰:Gradle 默认用目录名作为模块名,驼峰命名在跨平台时可能出问题。我习惯用短横线,比如
user-service。 - 不要忽略 settings.gradle.kts:有次我忘了在
settings.gradle.kts里include新模块,结果构建时 Gradle 完全无视它,找了半小时才发现。 - 慎用
compile和runtime:这两个配置在 Gradle 7.0 以后已经废弃了。用implementation、api、compileOnly替代。 - 版本目录文件位置别放错:
libs.versions.toml必须放在gradle/目录下,不是根目录。放错了 Gradle 不会报错,但版本目录不会生效。
核心要点总结:
- 按业务边界拆分模块,避免过细或过粗
- 模块间依赖保持单向,杜绝循环引用
- 用 Version Catalog 统一管理版本号
- 根项目负责继承配置,子模块负责业务实现
- 聚合让所有模块可以一键构建
多模块项目,说白了就是「分而治之」再加「统一管理」。分得好,项目结构清晰、构建高效;分得不好,那就是给自己挖坑。希望这些经验能帮你少走弯路。