30、实战项目:完整多模块项目搭建、插件开发与发布、CI/CD流水线

终于到了最后一章。说实话,前面讲了那么多零散的知识点,都是为这一刻做准备的。今天咱们就把所有东西串起来,完整走一遍——从零搭建一个多模块项目,写一个自定义插件并发布,最后配上CI/CD流水线。

这个流程我去年在团队里推过一次,踩了不少坑。嗯,今天我把这些经验都揉进去,你跟着走一遍,基本就能应付大部分实际场景了。

项目结构设计

先说说项目怎么拆。我个人习惯按功能边界来分模块,而不是按层。举个例子,一个电商系统,我不会拆成daoservicecontroller,而是拆成orderpaymentuser这种业务模块。

来看看我们这次要搭的结构:

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")

注意includeBuildinclude的区别。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 核心流程 1. 多模块项目搭建 settings.gradle.kts 模块拆分与依赖管理 2. 插件开发与发布 build-logic 模块 Plugin + Extension 3. CI/CD 流水线 GitHub Actions 自动构建 + 测试 + 发布 项目搭建细节 • include / includeBuild 区别 • 模块路径映射 • 全局属性 gradle.properties • 依赖版本统一管理 • 子模块 build.gradle.kts • 复合构建 vs 多项目构建 插件开发细节 • Plugin<Project> 接口 • Extension 扩展配置 • afterEvaluate 时机 • maven-publish 发布 • 本地仓库引用 • 插件版本管理 CI/CD 细节 • GitHub Actions 配置 • JDK 版本选择 • Gradle 缓存优化 • 条件触发发布 • 多分支策略 • 失败通知与回滚 三者结合,形成完整的自动化构建体系

核心要点:多模块项目解决的是代码组织问题,插件解决的是构建逻辑复用问题,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其实没那么复杂,关键是理解它的设计思路:类型安全、约定优于配置、可扩展。

希望这些内容对你有帮助。以后在项目里遇到构建相关的问题,欢迎随时交流。


公众号:蓝海资料掘金营,微信deep3321