第 3 章:Gradle 进阶工程能力

本章目标

本章解决:当项目变大、模块变多、团队变多后,如何把 Gradle 从"能用"提升到"可维护、可复用、可扩展"。

你会学习:

  • 内置 Task 类型(Copy / Sync / Zip / Exec)
  • 自定义 Task 完整输入输出声明与增量执行
  • 懒配置与 Provider API
  • afterEvaluate 陷阱与替代方案
  • buildSrcbuild-logic 的选择
  • Convention Plugin(Groovy & Kotlin DSL 双版本)
  • 插件 Extension 扩展点设计
  • Version Catalog 进阶
  • 构建缓存、配置缓存、并行构建
  • Composite Build 多仓库场景
  • CI 中的 Gradle 最佳实践

1. 内置 Task 类型

Gradle 提供了大量内置 task 类型,不需要从零写逻辑。

Copy Task

groovy 复制代码
tasks.register('copyConfigs', Copy) {
    group = 'build'
    description = '将配置文件复制到构建目录'
    from 'src/main/resources/config'
    into layout.buildDirectory.dir('config')
    include '*.yml', '*.properties'
    // 复制时替换占位符
    filter { line ->
        line.replace('${app.version}', project.version.toString())
    }
}

Sync Task

SyncCopy 类似,但会删除目标目录中不在来源中的文件,保持目录"同步":

groovy 复制代码
tasks.register('syncDist', Sync) {
    from configurations.runtimeClasspath
    from jar
    into layout.buildDirectory.dir('dist/lib')
}

Zip Task

groovy 复制代码
tasks.register('packageRelease', Zip) {
    group = 'distribution'
    archiveFileName = "${project.name}-${project.version}-release.zip"
    destinationDirectory = layout.buildDirectory.dir('distributions')
    from layout.buildDirectory.dir('install')
    from('README.md') { into 'docs' }
}

Exec Task

执行外部命令:

groovy 复制代码
tasks.register('runLint', Exec) {
    group = 'verification'
    commandLine 'sh', '-c', 'echo "Running lint check..."'
    // 在特定目录执行
    workingDir project.projectDir
}

Delete Task

groovy 复制代码
tasks.register('cleanGenerated', Delete) {
    delete layout.buildDirectory.dir('generated')
    delete fileTree(dir: 'src/main/java', include: '**/*Generated.java')
}

2. 自定义 Task 类型(完整输入输出声明)

自定义 task 类要声明输入输出,才能享受增量构建和缓存。

groovy 复制代码
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*

abstract class GenerateBuildInfo extends DefaultTask {

    @Input
    abstract Property<String> getVersionName()

    @Input
    abstract Property<String> getBuildProfile()

    @InputFile
    @Optional
    abstract RegularFileProperty getTemplateFile()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void generate() {
        def content = """\
version=${versionName.get()}
profile=${buildProfile.get()}
buildTime=${new Date().format('yyyy-MM-dd HH:mm:ss')}
""".stripIndent()
        outputFile.get().asFile.text = content
    }
}

注册并使用:

groovy 复制代码
tasks.register('generateBuildInfo', GenerateBuildInfo) {
    versionName = project.version.toString()
    buildProfile = providers.environmentVariable('BUILD_PROFILE').orElse('local')
    outputFile = layout.buildDirectory.file('generated/build-info.properties')
}

// 让 processResources 依赖该 task,自动打包到 jar 里
processResources.dependsOn generateBuildInfo
sourceSets.main.resources.srcDir layout.buildDirectory.dir('generated')

常用注解说明

注解 说明
@Input 影响输出的标量输入(String、Int、Boolean 等)
@InputFile 单个文件输入
@InputFiles 多个文件输入
@InputDirectory 目录输入
@OutputFile 单个文件输出
@OutputDirectory 目录输出
@Optional 该输入/输出不是必须的
@Internal 不影响缓存的内部属性
@Classpath classpath 输入(特殊哈希逻辑)

3. 增量 Task(InputChanges)

增量 task 可以只处理发生变化的文件,而不是每次全量处理:

groovy 复制代码
abstract class IncrementalProcessor extends DefaultTask {

    @Incremental
    @InputDirectory
    abstract DirectoryProperty getInputDir()

    @OutputDirectory
    abstract DirectoryProperty getOutputDir()

    @TaskAction
    void process(InputChanges changes) {
        if (!changes.incremental) {
            println '首次全量处理...'
        }

        changes.getFileChanges(inputDir).each { change ->
            if (change.changeType == ChangeType.REMOVED) {
                def target = new File(outputDir.get().asFile, change.normalizedPath)
                target.delete()
                return
            }
            println "处理变化文件: ${change.file.name} [${change.changeType}]"
            // 实际处理逻辑
            def output = new File(outputDir.get().asFile, change.normalizedPath)
            output.parentFile.mkdirs()
            output.text = change.file.text.toUpperCase()
        }
    }
}

4. 懒配置与 Provider API

懒配置核心原则

配置阶段要"声明意图",不要"立即计算结果"。

groovy 复制代码
// ❌ 错误:立即计算,配置阶段就触发文件 IO
def version = file('version.txt').text.trim()

tasks.register('printVersion') {
    doLast { println version }
}

// ✅ 正确:懒加载,只有 task 执行时才读文件
def versionProvider = providers.fileContents(
    layout.projectDirectory.file('version.txt')
).asText.map { it.trim() }

tasks.register('printVersion') {
    doLast { println versionProvider.get() }
}

Provider API 常用方法

groovy 复制代码
// 从环境变量读取
def profile = providers.environmentVariable('APP_PROFILE').orElse('dev')

// 从系统属性读取
def debug = providers.systemProperty('debug').map { it.toBoolean() }.orElse(false)

// 从 gradle.properties 读取
def maxHeap = providers.gradleProperty('maxHeap').orElse('2g')

// 组合 provider
def appName = providers.provider {
    "${project.name}-${project.version}"
}

// 在 task 中使用
tasks.register('printInfo') {
    // 声明 task 依赖的 provider(让 UP-TO-DATE 检查生效)
    inputs.property('profile', profile)
    doLast {
        println "profile=${profile.get()}, app=${appName.get()}"
    }
}

5. afterEvaluate 陷阱与替代方案

afterEvaluate 在所有项目配置完成后执行,常被滥用:

groovy 复制代码
// ❌ 常见错误用法:用 afterEvaluate 读取其他项目属性
afterEvaluate {
    // 看起来能用,但在复杂多模块中执行顺序不可靠
    println project.version
    tasks.named('test').configure {
        maxParallelForks = 4
    }
}

替代方案:使用懒配置 API,直接用 tasks.namedtasks.withType

groovy 复制代码
// ✅ 正确:直接用懒配置,不需要 afterEvaluate
tasks.withType(Test).configureEach {
    maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
    useJUnitPlatform()
}

tasks.named('jar') {
    manifest {
        attributes 'Main-Class': 'com.example.Main'
    }
}

什么时候 afterEvaluate 是合理的:

  • 插件需要在用户配置完成后做最终决策。
  • 跨模块读取另一个项目的属性(但要控制执行顺序)。

6. buildSrc

buildSrc 是 Gradle 内置的构建逻辑目录,放在这里的代码会自动编译,并在主构建脚本中可用。

结构:

text 复制代码
buildSrc/
├── build.gradle
└── src/main/groovy/
    ├── MyCustomTask.groovy
    └── com.example.java-conventions.gradle

buildSrc/build.gradle

groovy 复制代码
plugins {
    id 'groovy-gradle-plugin'
}

repositories {
    gradlePluginPortal()
}

优点:

  • 无需配置,Gradle 自动识别。
  • 适合小项目快速抽取构建逻辑。

缺点:

  • buildSrc 任何变化都会让整个构建的配置缓存失效。
  • 不能跨仓库复用。

7. build-logic 与 Convention Plugin

现代多模块项目更推荐 build-logic,通过 Composite Build 引入。

目录结构

text 复制代码
build-logic/
├── settings.gradle
├── build.gradle
└── src/main/groovy/
    ├── com.example.java-conventions.gradle
    └── com.example.quality-conventions.gradle

build-logic/settings.gradle

groovy 复制代码
dependencyResolutionManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
    }
}
rootProject.name = 'build-logic'

build-logic/build.gradle

groovy 复制代码
plugins {
    id 'groovy-gradle-plugin'
}

Convention Plugin(Groovy DSL)

com.example.java-conventions.gradle

groovy 复制代码
plugins {
    id 'java-library'
    id 'maven-publish'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

tasks.withType(JavaCompile).configureEach {
    options.encoding = 'UTF-8'
    options.release = 17
}

tasks.withType(Test).configureEach {
    useJUnitPlatform()
    testLogging {
        events 'passed', 'skipped', 'failed'
    }
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
}

Convention Plugin(Kotlin DSL)

com.example.java-conventions.gradle.kts

kotlin 复制代码
plugins {
    `java-library`
    `maven-publish`
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(17))
    }
}

tasks.withType<JavaCompile>().configureEach {
    options.encoding = "UTF-8"
    options.release.set(17)
}

tasks.withType<Test>().configureEach {
    useJUnitPlatform()
    testLogging {
        events("passed", "skipped", "failed")
    }
}

publishing {
    publications {
        create<MavenPublication>("mavenJava") {
            from(components["java"])
        }
    }
}

引入方式

settings.gradle

groovy 复制代码
pluginManagement {
    includeBuild 'build-logic'
}

子模块 build.gradle

groovy 复制代码
plugins {
    id 'com.example.java-conventions'
}

8. 插件 Extension 扩展点

插件可以暴露 Extension 让用户配置:

groovy 复制代码
// 定义 Extension 类
abstract class CompanyPluginExtension {
    abstract Property<String> getTeamName()
    abstract Property<Boolean> getEnableQualityGate()

    CompanyPluginExtension() {
        teamName.convention('default-team')
        enableQualityGate.convention(true)
    }
}

// 注册 Extension 并在插件中使用
class CompanyPlugin implements Plugin<Project> {
    void apply(Project project) {
        def extension = project.extensions.create('companyConfig', CompanyPluginExtension)

        project.tasks.register('printTeamInfo') {
            doLast {
                println "Team: ${extension.teamName.get()}"
                println "Quality Gate: ${extension.enableQualityGate.get()}"
            }
        }
    }
}

用户在 build.gradle 中:

groovy 复制代码
companyConfig {
    teamName = 'platform-team'
    enableQualityGate = true
}

9. Version Catalog 进阶

声明 bundles(依赖组)

toml 复制代码
[versions]
junit = "5.10.2"
mockito = "5.11.0"

[libraries]
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-params  = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
mockito-core  = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockito-junit = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }

[bundles]
testing = ["junit-jupiter", "junit-params", "mockito-core", "mockito-junit"]

[plugins]
spring-boot = { id = "org.springframework.boot", version = "3.3.0" }

使用 bundle:

groovy 复制代码
dependencies {
    testImplementation libs.bundles.testing
}

使用 catalog 中的 plugin:

groovy 复制代码
plugins {
    alias(libs.plugins.spring.boot)
}

10. 构建缓存

构建缓存复用历史 task 输出。开启:

properties 复制代码
org.gradle.caching=true

执行时强制使用:

bash 复制代码
gradle clean build --build-cache

查看缓存命中情况:

bash 复制代码
gradle build --info 2>&1 | grep -E 'UP-TO-DATE|FROM-CACHE|cache'

自定义 task 缓存:

groovy 复制代码
abstract class GenerateBuildInfo extends DefaultTask {
    // 必须声明 @Input @OutputFile 才能缓存
    @Input abstract Property<String> getVersionName()
    @OutputFile abstract RegularFileProperty getOutputFile()

    @TaskAction
    void generate() {
        outputFile.get().asFile.text = "version=${versionName.get()}"
    }
}

// 标记为可缓存
tasks.register('generateBuildInfo', GenerateBuildInfo) {
    outputs.cacheIf { true }
    versionName = project.version.toString()
    outputFile = layout.buildDirectory.file('generated/build-info.properties')
}

11. 配置缓存

配置缓存复用配置阶段结果,大型项目中能显著减少冷启动时间。

开启:

properties 复制代码
org.gradle.configuration-cache=true

验证:

bash 复制代码
gradle help --configuration-cache

常见不兼容原因:

问题 解决方向
task 持有 Project 对象 改用 ProviderLayoutObjectFactory
配置阶段读取文件 改用 providers.fileContents
使用不可序列化对象 改用 Gradle 提供的类型
在配置阶段做网络请求 移到 task 执行阶段

12. 并行构建

properties 复制代码
org.gradle.parallel=true

并行构建适合多模块项目。Gradle 会在依赖关系允许的情况下并行执行不同模块的 task。

配合 worker API(在单个 task 内并行):

groovy 复制代码
abstract class ParallelProcessor extends DefaultTask {
    @Inject
    abstract WorkerExecutor getWorkerExecutor()

    @TaskAction
    void process() {
        def queue = workerExecutor.noIsolation()
        ['a', 'b', 'c'].each { item ->
            queue.submit(ProcessAction) { params ->
                params.item.set(item)
            }
        }
    }
}

13. Composite Build(多仓库)

Composite Build 允许把另一个独立 Gradle 项目作为本项目的依赖,在本地联调而不需要发布。

场景:my-app 依赖 my-library,两个独立 Git 仓库,本地同时开发。

my-app/settings.gradle

groovy 复制代码
// 用本地 my-library 替代从 Maven 仓库下载
includeBuild '../my-library'

my-app/build.gradle

groovy 复制代码
dependencies {
    // 坐标与 my-library 发布坐标一致,Gradle 自动用本地版本
    implementation 'com.example:my-library:1.0.0'
}

好处:

  • 不需要频繁 publishToMavenLocal
  • 修改 my-library 后,my-app 自动重新编译。

14. CI 中的 Gradle 实践

基础流水线

yaml 复制代码
# GitHub Actions
name: Gradle Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17
      - uses: gradle/actions/setup-gradle@v4
        with:
          cache-read-only: ${{ github.ref != 'refs/heads/main' }}
      - run: ./gradlew clean build --stacktrace
      - name: Upload test reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-reports
          path: '**/build/reports/tests/'

关键 CI 配置

缓存 Gradle User Home(setup-gradle action 自动处理):

bash 复制代码
# CI 推荐命令
./gradlew clean build --scan --stacktrace --no-daemon

--no-daemon 在短生命周期 CI 容器中避免 Daemon 残留。


15. 实操:验证 demo 的 Convention Plugin

bash 复制代码
cd demo/gradle-multi-module-demo

# 查看 Convention Plugin 定义
cat build-logic/src/main/groovy/com.example.java-conventions.gradle

# 查看子模块的精简 build.gradle
cat service/build.gradle
# 只有 2 行:引用 convention + 声明依赖

# 运行测试(Convention Plugin 提供了测试配置)
gradle :service:test

# 查看所有 task(Convention Plugin 注册的 publish task)
gradle :common:tasks

验证点:

  • service/build.gradle 非常精简,Java 版本、编码、测试配置全来自 Convention Plugin。
  • gradle :common:tasks 能看到 publish 相关 task(Convention Plugin 添加的)。

16. 常见问题

问题 1:为什么不要在每个子模块复制同样配置

复制配置短期快,长期会导致版本不一致、升级困难、排查困难。超过 3 个模块共享同类配置时,就应该考虑抽取 Convention Plugin。

问题 2:为什么修改 build-logic 后构建变慢

构建逻辑本身也是代码。修改后需要重新编译插件,并使相关配置缓存失效。构建逻辑应该稳定、清晰、少变。

问题 3:什么时候用 buildSrc,什么时候用 build-logic

场景 推荐
小项目,少量构建工具类 buildSrc
多模块项目,构建规范需要长期维护 build-logic
多仓库共享构建插件 独立 Gradle 插件项目

问题 4:Provider 和直接读属性有什么区别

直接读属性在配置阶段立即计算,即使 task 从未执行也会触发。Provider 是懒加载,只有真正需要值时才计算。大型项目中滥用立即计算会显著拖慢配置阶段。

问题 5:配置缓存和构建缓存有什么区别

缓存类型 复用内容 作用
构建缓存 task 输出文件 跳过已执行且输入未变的 task
配置缓存 配置阶段结果 跳过整个配置阶段

两者可以同时开启,效果叠加。

相关推荐
空中海3 小时前
第 2 章:Gradle 项目构建实战
gradle
空中海11 小时前
第 1 章:Gradle 入门基础
gradle
空中海13 小时前
第 4 章:Gradle 专家级实践
gradle
黄林晴1 天前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
followYouself8 天前
Gradle、AGP、Plugin插件基本知识
android·gradle·plugin·agp
千码君201611 天前
Flutter:在win10上第一次安装和尝试开发记录
flutter·gradle·android-studio·安卓模拟器
Ww.xh20 天前
Flutter配置Gradle完整教程
flutter·gradle·android studio
vortex520 天前
Gradle 从入门到实战
java·gradle
蜡台24 天前
Android Studio Gradlew JDK配置
java·gradle·android studio·intellij-idea