30、实战项目:完整多模块项目搭建、插件开发与发布、CI/CD流水线
终于到了最后一章。说实话,前面讲了那么多零散的知识点,都是为这一刻做准备的。今天咱们就把所有东西串起来,完整走一遍——从零搭建一个多模块项目,写一个自定义插件并发布,最后配上CI/CD流水线。
这个流程我去年在团队里推过一次,踩了不少坑。嗯,今天我把这些经验都揉进去,你跟着走一遍,基本就能应付大部分实际场景了。
项目结构设计
先说说项目怎么拆。我个人习惯按功能边界来分模块,而不是按层。举个例子,一个电商系统,我不会拆成dao、service、controller,而是拆成order、payment、user这种业务模块。
来看看我们这次要搭的结构:
my-platform/
├── build.gradle.kts # 根项目构建脚本
├── settings.gradle.kts # 模块声明
├── gradle.properties # 全局属性
├── build-logic/ # 构建逻辑模块(插件源码)
│ ├── build.gradle.kts
│ └── src/main/kotlin/
│ └── com/example/
│ ├── MyPlugin.kt
│ └── MyExtension.kt
├── module-common/ # 公共模块
│ ├── build.gradle.kts
│ └── src/
├── module-service/ # 服务模块
│ ├── build.gradle.kts
│ └── src/
└── module-web/ # Web入口模块
├── build.gradle.kts
└── src/
这里有个关键点:build-logic是一个独立的构建模块,专门放插件代码。这样做的好处是——插件和业务代码解耦,发布也方便。
多模块的settings配置
先看settings.gradle.kts怎么写。我记得刚开始用Kotlin DSL时,总把模块路径写错,后来养成了一个习惯:先写include,再写projectDir映射。
// settings.gradle.kts
rootProject.name = "my-platform"
include("module-common")
include("module-service")
include("module-web")
// 构建逻辑模块,不参与业务发布
includeBuild("build-logic")
// 自定义模块路径(如果模块不在根目录下)
// project(":module-web").projectDir = file("apps/web")
注意includeBuild和include的区别。includeBuild是把整个项目当作复合构建引入,插件可以直接被其他模块引用。说白了,就是本地开发时不用先发布插件。
插件开发:从需求到实现
我们写一个插件,功能很简单:自动给每个模块注入公共依赖,并检查模块命名规范。
先看build-logic/build.gradle.kts:
// build-logic/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
// 插件依赖的Gradle API版本
gradlePlugin {
plugins {
register("my-platform-plugin") {
id = "com.example.my-platform"
implementationClass = "com.example.MyPlugin"
}
}
}
然后写插件本体。我习惯把扩展类和插件类分开,这样结构清晰:
// MyExtension.kt
open class MyExtension {
var enableCheck: Boolean = true
var commonDependencies: List<String> = listOf()
}
// MyPlugin.kt
class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
val extension = target.extensions.create("myPlatform", MyExtension::class.java)
// 配置阶段结束后执行
target.afterEvaluate {
if (extension.enableCheck) {
checkModuleName(target)
}
applyCommonDependencies(target, extension)
}
}
private fun checkModuleName(project: Project) {
val name = project.name
require(name.startsWith("module-")) {
"模块名必须以'module-'开头,当前: $name"
}
println("✅ 模块名检查通过: $name")
}
private fun applyCommonDependencies(project: Project, ext: MyExtension) {
val deps = ext.commonDependencies
if (deps.isNotEmpty()) {
project.dependencies {
deps.forEach { dep ->
add("implementation", dep)
}
}
println("📦 已注入公共依赖: $deps")
}
}
}
这里有个坑——afterEvaluate的时机。我曾经在apply里直接操作依赖,结果发现有些依赖还没解析。后来改成afterEvaluate,问题就解决了。
插件发布到本地仓库
发布插件其实不复杂。在build-logic/build.gradle.kts里加上发布配置:
plugins {
`kotlin-dsl`
`maven-publish`
}
publishing {
publications {
create<MavenPublication>("plugin") {
from(components["kotlin"])
groupId = "com.example"
artifactId = "my-platform-plugin"
version = "1.0.0"
}
}
repositories {
maven {
// 发布到本地仓库
url = uri("${rootProject.projectDir}/repo")
}
}
}
执行./gradlew :build-logic:publish,插件就会发布到项目根目录的repo文件夹下。其他项目引用时,只需要在settings.gradle.kts里加上:
pluginManagement {
repositories {
maven {
url = uri("file://${rootProject.projectDir}/repo")
}
gradlePluginPortal()
}
}
然后在模块的build.gradle.kts里:
plugins {
id("com.example.my-platform") version "1.0.0"
}
myPlatform {
enableCheck = true
commonDependencies = listOf(
"org.jetbrains.kotlin:kotlin-stdlib",
"com.google.guava:guava:32.1.2-jre"
)
}
你看,这样每个模块就不用重复写公共依赖了。维护起来也方便,改一处全生效。
CI/CD流水线配置
流水线我用GitHub Actions举例。说白了,就是自动化构建、测试、发布。
在项目根目录创建.github/workflows/build.yml:
name: Build and Publish
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Gradle
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
- name: Build and Test
run: ./gradlew build test --no-daemon
- name: Publish Plugin
if: github.ref == 'refs/heads/main'
run: ./gradlew :build-logic:publish --no-daemon
这里我加了缓存步骤。为什么?因为Gradle的依赖下载太慢了,不加缓存每次跑都要等几分钟。我曾经有个项目没配缓存,CI跑了半年,浪费了多少时间你想想看。
核心知识体系
下面这张图,我把整个流程的核心逻辑画出来了:
核心要点:多模块项目解决的是代码组织问题,插件解决的是构建逻辑复用问题,CI/CD解决的是自动化问题。三者缺一不可。
避坑指南
最后分享几个我踩过的坑:
- 插件类加载冲突:如果
build-logic和业务模块用了不同版本的Gradle API,会出现NoClassDefFoundError。解决办法是统一Gradle版本,或者在build-logic里用compileOnly依赖。 - CI环境中文乱码:GitHub Actions的Ubuntu镜像默认没有中文字体。如果你的插件输出了中文日志,记得设置
LC_ALL=en_US.UTF-8。 - 缓存失效问题:Gradle的缓存key如果只包含
*.gradle.kts,改动gradle.properties时缓存不会失效。建议把gradle.properties也加入hash计算。
小技巧:在build-logic里写单元测试,验证插件的逻辑是否正确。用ProjectBuilder模拟项目对象,比手动跑构建快得多。
注意:发布插件到公共仓库(如Gradle Plugin Portal)需要审核,流程比较长。建议先在本地或私有仓库测试,确认无误后再发布到公共仓库。
好了,这一章的内容就到这里。整个课程的知识点,在这一章里全部串起来了。你如果跟着走一遍,应该能感受到——Gradle Kotlin DSL其实没那么复杂,关键是理解它的设计思路:类型安全、约定优于配置、可扩展。
希望这些内容对你有帮助。以后在项目里遇到构建相关的问题,欢迎随时交流。