本章目标
本章解决:当项目变大、模块变多、团队变多后,如何把 Gradle 从"能用"提升到"可维护、可复用、可扩展"。
你会学习:
- 内置 Task 类型(Copy / Sync / Zip / Exec)
- 自定义 Task 完整输入输出声明与增量执行
- 懒配置与 Provider API
afterEvaluate陷阱与替代方案buildSrc与build-logic的选择- Convention Plugin(Groovy & Kotlin DSL 双版本)
- 插件 Extension 扩展点设计
- Version Catalog 进阶
- 构建缓存、配置缓存、并行构建
- Composite Build 多仓库场景
- CI 中的 Gradle 最佳实践
1. 内置 Task 类型
Gradle 提供了大量内置 task 类型,不需要从零写逻辑。
Copy Task
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
Sync和Copy类似,但会删除目标目录中不在来源中的文件,保持目录"同步":
tasks.register('syncDist',Sync){from configurations.runtimeClasspath from jar into layout.buildDirectory.dir('dist/lib')}Zip Task
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
执行外部命令:
tasks.register('runLint',Exec){group='verification'commandLine'sh','-c','echo "Running lint check..."'// 在特定目录执行workingDir project.projectDir}Delete Task
tasks.register('cleanGenerated',Delete){delete layout.buildDirectory.dir('generated')deletefileTree(dir:'src/main/java',include:'**/*Generated.java')}2. 自定义 Task 类型(完整输入输出声明)
自定义 task 类要声明输入输出,才能享受增量构建和缓存。
importorg.gradle.api.DefaultTaskimportorg.gradle.api.file.RegularFilePropertyimportorg.gradle.api.provider.Propertyimportorg.gradle.api.tasks.*abstractclassGenerateBuildInfoextendsDefaultTask{@InputabstractProperty<String>getVersionName()@InputabstractProperty<String>getBuildProfile()@InputFile@OptionalabstractRegularFilePropertygetTemplateFile()@OutputFileabstractRegularFilePropertygetOutputFile()@TaskActionvoidgenerate(){defcontent="""\ version=${versionName.get()}profile=${buildProfile.get()}buildTime=${newDate().format('yyyy-MM-dd HH:mm:ss')}""".stripIndent()outputFile.get().asFile.text=content}}注册并使用:
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 可以只处理发生变化的文件,而不是每次全量处理:
abstractclassIncrementalProcessorextendsDefaultTask{@Incremental@InputDirectoryabstractDirectoryPropertygetInputDir()@OutputDirectoryabstractDirectoryPropertygetOutputDir()@TaskActionvoidprocess(InputChanges changes){if(!changes.incremental){println'首次全量处理...'}changes.getFileChanges(inputDir).each{change->if(change.changeType==ChangeType.REMOVED){deftarget=newFile(outputDir.get().asFile,change.normalizedPath)target.delete()return}println"处理变化文件:${change.file.name}[${change.changeType}]"// 实际处理逻辑defoutput=newFile(outputDir.get().asFile,change.normalizedPath)output.parentFile.mkdirs()output.text=change.file.text.toUpperCase()}}}4. 懒配置与 Provider API
懒配置核心原则
配置阶段要"声明意图",不要"立即计算结果"。
// ❌ 错误:立即计算,配置阶段就触发文件 IOdefversion=file('version.txt').text.trim()tasks.register('printVersion'){doLast{println version}}// ✅ 正确:懒加载,只有 task 执行时才读文件defversionProvider=providers.fileContents(layout.projectDirectory.file('version.txt')).asText.map{it.trim()}tasks.register('printVersion'){doLast{println versionProvider.get()}}Provider API 常用方法
// 从环境变量读取defprofile=providers.environmentVariable('APP_PROFILE').orElse('dev')// 从系统属性读取defdebug=providers.systemProperty('debug').map{it.toBoolean()}.orElse(false)// 从 gradle.properties 读取defmaxHeap=providers.gradleProperty('maxHeap').orElse('2g')// 组合 providerdefappName=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在所有项目配置完成后执行,常被滥用:
// ❌ 常见错误用法:用 afterEvaluate 读取其他项目属性afterEvaluate{// 看起来能用,但在复杂多模块中执行顺序不可靠println project.version tasks.named('test').configure{maxParallelForks=4}}替代方案:使用懒配置 API,直接用tasks.named和tasks.withType:
// ✅ 正确:直接用懒配置,不需要 afterEvaluatetasks.withType(Test).configureEach{maxParallelForks=Runtime.runtime.availableProcessors().intdiv(2)?:1useJUnitPlatform()}tasks.named('jar'){manifest{attributes'Main-Class':'com.example.Main'}}什么时候afterEvaluate是合理的:
- 插件需要在用户配置完成后做最终决策。
- 跨模块读取另一个项目的属性(但要控制执行顺序)。
6.buildSrc
buildSrc是 Gradle 内置的构建逻辑目录,放在这里的代码会自动编译,并在主构建脚本中可用。
结构:
buildSrc/ ├── build.gradle └── src/main/groovy/ ├── MyCustomTask.groovy └── com.example.java-conventions.gradlebuildSrc/build.gradle:
plugins{id'groovy-gradle-plugin'}repositories{gradlePluginPortal()}优点:
- 无需配置,Gradle 自动识别。
- 适合小项目快速抽取构建逻辑。
缺点:
buildSrc任何变化都会让整个构建的配置缓存失效。- 不能跨仓库复用。
7.build-logic与 Convention Plugin
现代多模块项目更推荐build-logic,通过 Composite Build 引入。
目录结构
build-logic/ ├── settings.gradle ├── build.gradle └── src/main/groovy/ ├── com.example.java-conventions.gradle └── com.example.quality-conventions.gradlebuild-logic/settings.gradle:
dependencyResolutionManagement{repositories{gradlePluginPortal()mavenCentral()}}rootProject.name='build-logic'build-logic/build.gradle:
plugins{id'groovy-gradle-plugin'}Convention Plugin(Groovy DSL)
com.example.java-conventions.gradle:
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:
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:
pluginManagement{includeBuild'build-logic'}子模块build.gradle:
plugins{id'com.example.java-conventions'}8. 插件 Extension 扩展点
插件可以暴露 Extension 让用户配置:
// 定义 Extension 类abstractclassCompanyPluginExtension{abstractProperty<String>getTeamName()abstractProperty<Boolean>getEnableQualityGate()CompanyPluginExtension(){teamName.convention('default-team')enableQualityGate.convention(true)}}// 注册 Extension 并在插件中使用classCompanyPluginimplementsPlugin<Project>{voidapply(Project project){defextension=project.extensions.create('companyConfig',CompanyPluginExtension)project.tasks.register('printTeamInfo'){doLast{println"Team:${extension.teamName.get()}"println"Quality Gate:${extension.enableQualityGate.get()}"}}}}用户在build.gradle中:
companyConfig{teamName='platform-team'enableQualityGate=true}9. Version Catalog 进阶
声明 bundles(依赖组)
[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:
dependencies{testImplementation libs.bundles.testing}使用 catalog 中的 plugin:
plugins{alias(libs.plugins.spring.boot)}10. 构建缓存
构建缓存复用历史 task 输出。开启:
org.gradle.caching=true执行时强制使用:
gradle clean build --build-cache查看缓存命中情况:
gradle build--info2>&1|grep-E'UP-TO-DATE|FROM-CACHE|cache'自定义 task 缓存:
abstractclassGenerateBuildInfoextendsDefaultTask{// 必须声明 @Input @OutputFile 才能缓存@InputabstractProperty<String>getVersionName()@OutputFileabstractRegularFilePropertygetOutputFile()@TaskActionvoidgenerate(){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. 配置缓存
配置缓存复用配置阶段结果,大型项目中能显著减少冷启动时间。
开启:
org.gradle.configuration-cache=true验证:
gradlehelp--configuration-cache常见不兼容原因:
| 问题 | 解决方向 |
|---|---|
task 持有Project对象 | 改用Provider、Layout、ObjectFactory |
| 配置阶段读取文件 | 改用providers.fileContents |
| 使用不可序列化对象 | 改用 Gradle 提供的类型 |
| 在配置阶段做网络请求 | 移到 task 执行阶段 |
12. 并行构建
org.gradle.parallel=true并行构建适合多模块项目。Gradle 会在依赖关系允许的情况下并行执行不同模块的 task。
配合 worker API(在单个 task 内并行):
abstractclassParallelProcessorextendsDefaultTask{@InjectabstractWorkerExecutorgetWorkerExecutor()@TaskActionvoidprocess(){defqueue=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:
// 用本地 my-library 替代从 Maven 仓库下载includeBuild'../my-library'my-app/build.gradle:
dependencies{// 坐标与 my-library 发布坐标一致,Gradle 自动用本地版本implementation'com.example:my-library:1.0.0'}好处:
- 不需要频繁
publishToMavenLocal。 - 修改
my-library后,my-app自动重新编译。
14. CI 中的 Gradle 实践
基础流水线
# GitHub Actionsname:Gradle Buildon:[push,pull_request]jobs:build:runs-on:ubuntu-lateststeps:-uses:actions/checkout@v4-uses:actions/setup-java@v4with:distribution:temurinjava-version:17-uses:gradle/actions/setup-gradle@v4with:cache-read-only:${{github.ref!='refs/heads/main'}}-run:./gradlew clean build--stacktrace-name:Upload test reportsif:always()uses:actions/upload-artifact@v4with:name:test-reportspath:'**/build/reports/tests/'关键 CI 配置
缓存 Gradle User Home(setup-gradleaction 自动处理):
# CI 推荐命令./gradlew clean build--scan--stacktrace--no-daemon--no-daemon在短生命周期 CI 容器中避免 Daemon 残留。
15. 实操:验证 demo 的 Convention Plugin
cddemo/gradle-multi-module-demo# 查看 Convention Plugin 定义catbuild-logic/src/main/groovy/com.example.java-conventions.gradle# 查看子模块的精简 build.gradlecatservice/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 |
| 配置缓存 | 配置阶段结果 | 跳过整个配置阶段 |
两者可以同时开启,效果叠加。