9、多模块项目:多项目结构、跨项目配置、依赖替换
说实话,多模块项目是我在实战中遇到最多坑的地方。刚开始用 Gradle 时,我习惯把所有代码塞进一个模块里,觉得这样简单。直到项目膨胀到几万行代码,每次编译都要等五分钟……嗯,那感觉真不好受。
多模块不是炫技,而是工程化的必然选择。今天我们就来聊聊,怎么用 Kotlin DSL 把多模块项目组织得既清晰又灵活。
9.1 多项目结构:从单模块到多模块
先看一个典型的多模块项目长什么样。我个人习惯用 settings.gradle.kts 来声明所有子模块:
// settings.gradle.kts
rootProject.name = "my-app"
include(":core")
include(":service:user")
include(":service:order")
include(":web:api")
include(":web:admin")
注意这里的命名方式。冒号代表路径分隔符,:service:user 对应的是 service/user 目录。我见过有人把所有模块都平铺在根目录下,结果项目一多就乱成一锅粥。
目录结构建议这样组织:
my-app/
├── settings.gradle.kts
├── build.gradle.kts // 根项目构建脚本
├── core/
│ └── build.gradle.kts
├── service/
│ ├── user/
│ │ └── build.gradle.kts
│ └── order/
│ └── build.gradle.kts
└── web/
├── api/
│ └── build.gradle.kts
└── admin/
└── build.gradle.kts
为什么要分层?说白了,就是让模块之间的依赖关系一目了然。你想想看,如果所有模块都平铺,新人接手项目时根本搞不清哪个模块是核心,哪个是业务层。
9.2 跨项目配置:别再复制粘贴了
多模块项目最烦人的是什么?是每个子模块都要写一遍 kotlin("jvm")、application 插件,还要重复配置依赖版本。我曾经在一个 20 多个模块的项目里,看到每个 build.gradle.kts 都有一模一样的 30 行配置……
解决方案是用 allprojects 或 subprojects 做统一配置。但这里有个坑:allprojects 会把根项目也算进去,而根项目通常不需要应用业务插件。
我推荐的做法是:
// 根项目的 build.gradle.kts
subprojects {
apply(plugin = "kotlin")
apply(plugin = "java-library")
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
"implementation"(kotlin("stdlib"))
"testImplementation"(kotlin("test"))
}
}
这样所有子模块自动获得 Kotlin 支持和标准库依赖。但注意,subprojects 里的配置是共享的,如果某个模块需要特殊配置,可以在它自己的 build.gradle.kts 里覆盖。
9.3 依赖替换:解决冲突的终极武器
依赖冲突是每个多模块项目都会遇到的噩梦。比如模块 A 依赖了 okhttp:4.9.0,模块 B 依赖了 okhttp:4.10.0,Gradle 默认会选择最高版本。但有时候,高版本不兼容低版本的 API,这时候就需要手动干预。
依赖替换(Dependency Substitution)就是干这个的。它允许你在解析依赖时,把某个依赖换成另一个。我曾经在一个大型微服务项目里,用这个功能把多个模块的日志框架统一成了 Logback,避免了 SLF4J 绑定冲突。
看个例子:
// 在 settings.gradle.kts 中
dependencyResolutionManagement {
repositories {
mavenCentral()
}
versionCatalogs {
create("libs") {
from(files("gradle/libs.versions.toml"))
}
}
}
// 或者在 build.gradle.kts 中
configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute(module("com.google.guava:guava"))
.using(module("com.google.guava:guava:30.1-jre"))
.because("统一 Guava 版本,避免冲突")
}
}
}
这里要注意,dependencySubstitution 是在配置阶段执行的,所以它会影响所有依赖解析。如果你只想替换某个特定配置的依赖,可以用 configurations["compileClasspath"] 来限定范围。
9.4 跨项目依赖:模块间的正确引用方式
多模块之间互相依赖是常态。比如 :web:api 依赖 :service:user,:service:user 又依赖 :core。正确的做法是用 project() 函数:
// web/api/build.gradle.kts
dependencies {
implementation(project(":service:user"))
implementation(project(":core"))
}
// service/user/build.gradle.kts
dependencies {
implementation(project(":core"))
}
这里有个容易被忽略的点:project() 的参数是模块路径,必须和 settings.gradle.kts 里 include 的路径完全一致。大小写、冒号数量都不能错。
我建议在根项目里定义一个依赖映射,避免硬编码路径:
// 根项目的 build.gradle.kts
val modules = mapOf(
"core" to project(":core"),
"userService" to project(":service:user"),
"orderService" to project(":service:order")
)
// 子模块引用时
dependencies {
implementation(modules["core"])
}
这样如果模块路径变了,只需要改根项目一处即可。
9.5 知识体系总览
下面这张图总结了多模块项目的核心逻辑:
9.6 实战中的避坑指南
最后分享几个我踩过的坑:
- 不要滥用 allprojects:根项目通常不需要应用业务插件,用
subprojects更安全。 - 版本号统一管理:我习惯用
gradle.properties或版本目录文件,而不是在每个模块里硬编码版本号。 - 依赖替换要加注释:替换的原因一定要写清楚,不然三个月后你自己都看不懂为什么这么配。
- 模块间依赖不要循环:Gradle 会检测循环依赖并报错,但有时候间接循环很难发现。建议用
gradle dependencies命令定期检查。
核心总结:多模块项目的本质是「分而治之」。通过合理的模块划分、统一的跨项目配置、以及灵活的依赖替换,你可以让大型项目保持可维护性。记住,配置是手段,不是目的——别为了多模块而多模块。
公众号:蓝海资料掘金营,微信deep3321