
本章目标本章解决当项目变大、模块变多、团队变多后如何把 Gradle 从能用提升到可维护、可复用、可扩展。你会学习内置 Task 类型Copy / Sync / Zip / Exec自定义 Task 完整输入输出声明与增量执行懒配置与 Provider APIafterEvaluate陷阱与替代方案buildSrc与build-logic的选择Convention PluginGroovy Kotlin DSL 双版本插件 Extension 扩展点设计Version Catalog 进阶构建缓存、配置缓存、并行构建Composite Build 多仓库场景CI 中的 Gradle 最佳实践1. 内置 Task 类型Gradle 提供了大量内置 task 类型不需要从零写逻辑。Copy Tasktasks.register(copyConfigs,Copy){groupbuilddescription将配置文件复制到构建目录fromsrc/main/resources/configinto layout.buildDirectory.dir(config)include*.yml,*.properties// 复制时替换占位符filter{line-line.replace(${app.version},project.version.toString())}}Sync TaskSync和Copy类似但会删除目标目录中不在来源中的文件保持目录同步tasks.register(syncDist,Sync){from configurations.runtimeClasspath from jar into layout.buildDirectory.dir(dist/lib)}Zip Tasktasks.register(packageRelease,Zip){groupdistributionarchiveFileName${project.name}-${project.version}-release.zipdestinationDirectorylayout.buildDirectory.dir(distributions)from layout.buildDirectory.dir(install)from(README.md){intodocs}}Exec Task执行外部命令tasks.register(runLint,Exec){groupverificationcommandLinesh,-c,echo Running lint check...// 在特定目录执行workingDir project.projectDir}Delete Tasktasks.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{InputabstractPropertyStringgetVersionName()InputabstractPropertyStringgetBuildProfile()InputFileOptionalabstractRegularFilePropertygetTemplateFile()OutputFileabstractRegularFilePropertygetOutputFile()TaskActionvoidgenerate(){defcontent\ version${versionName.get()}profile${buildProfile.get()}buildTime${newDate().format(yyyy-MM-dd HH:mm:ss)}.stripIndent()outputFile.get().asFile.textcontent}}注册并使用tasks.register(generateBuildInfo,GenerateBuildInfo){versionNameproject.version.toString()buildProfileproviders.environmentVariable(BUILD_PROFILE).orElse(local)outputFilelayout.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不影响缓存的内部属性Classpathclasspath 输入特殊哈希逻辑3. 增量 TaskInputChanges增量 task 可以只处理发生变化的文件而不是每次全量处理abstractclassIncrementalProcessorextendsDefaultTask{IncrementalInputDirectoryabstractDirectoryPropertygetInputDir()OutputDirectoryabstractDirectoryPropertygetOutputDir()TaskActionvoidprocess(InputChanges changes){if(!changes.incremental){println首次全量处理...}changes.getFileChanges(inputDir).each{change-if(change.changeTypeChangeType.REMOVED){deftargetnewFile(outputDir.get().asFile,change.normalizedPath)target.delete()return}println处理变化文件:${change.file.name}[${change.changeType}]// 实际处理逻辑defoutputnewFile(outputDir.get().asFile,change.normalizedPath)output.parentFile.mkdirs()output.textchange.file.text.toUpperCase()}}}4. 懒配置与 Provider API懒配置核心原则配置阶段要声明意图不要立即计算结果。// ❌ 错误立即计算配置阶段就触发文件 IOdefversionfile(version.txt).text.trim()tasks.register(printVersion){doLast{println version}}// ✅ 正确懒加载只有 task 执行时才读文件defversionProviderproviders.fileContents(layout.projectDirectory.file(version.txt)).asText.map{it.trim()}tasks.register(printVersion){doLast{println versionProvider.get()}}Provider API 常用方法// 从环境变量读取defprofileproviders.environmentVariable(APP_PROFILE).orElse(dev)// 从系统属性读取defdebugproviders.systemProperty(debug).map{it.toBoolean()}.orElse(false)// 从 gradle.properties 读取defmaxHeapproviders.gradleProperty(maxHeap).orElse(2g)// 组合 providerdefappNameproviders.provider{${project.name}-${project.version}}// 在 task 中使用tasks.register(printInfo){// 声明 task 依赖的 provider让 UP-TO-DATE 检查生效inputs.property(profile,profile)doLast{printlnprofile${profile.get()}, app${appName.get()}}}5.afterEvaluate陷阱与替代方案afterEvaluate在所有项目配置完成后执行常被滥用// ❌ 常见错误用法用 afterEvaluate 读取其他项目属性afterEvaluate{// 看起来能用但在复杂多模块中执行顺序不可靠println project.version tasks.named(test).configure{maxParallelForks4}}替代方案使用懒配置 API直接用tasks.named和tasks.withType// ✅ 正确直接用懒配置不需要 afterEvaluatetasks.withType(Test).configureEach{maxParallelForksRuntime.runtime.availableProcessors().intdiv(2)?:1useJUnitPlatform()}tasks.named(jar){manifest{attributesMain-Class:com.example.Main}}什么时候afterEvaluate是合理的插件需要在用户配置完成后做最终决策。跨模块读取另一个项目的属性但要控制执行顺序。6.buildSrcbuildSrc是 Gradle 内置的构建逻辑目录放在这里的代码会自动编译并在主构建脚本中可用。结构buildSrc/ ├── build.gradle └── src/main/groovy/ ├── MyCustomTask.groovy └── com.example.java-conventions.gradlebuildSrc/build.gradleplugins{idgroovy-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.gradledependencyResolutionManagement{repositories{gradlePluginPortal()mavenCentral()}}rootProject.namebuild-logicbuild-logic/build.gradleplugins{idgroovy-gradle-plugin}Convention PluginGroovy DSLcom.example.java-conventions.gradleplugins{idjava-libraryidmaven-publish}java{toolchain{languageVersionJavaLanguageVersion.of(17)}}tasks.withType(JavaCompile).configureEach{options.encodingUTF-8options.release17}tasks.withType(Test).configureEach{useJUnitPlatform()testLogging{eventspassed,skipped,failed}}publishing{publications{mavenJava(MavenPublication){from components.java}}}Convention PluginKotlin DSLcom.example.java-conventions.gradle.ktsplugins{java-library maven-publish}java{toolchain{languageVersion.set(JavaLanguageVersion.of(17))}}tasks.withTypeJavaCompile().configureEach{options.encodingUTF-8options.release.set(17)}tasks.withTypeTest().configureEach{useJUnitPlatform()testLogging{events(passed,skipped,failed)}}publishing{publications{createMavenPublication(mavenJava){from(components[java])}}}引入方式根settings.gradlepluginManagement{includeBuildbuild-logic}子模块build.gradleplugins{idcom.example.java-conventions}8. 插件 Extension 扩展点插件可以暴露 Extension 让用户配置// 定义 Extension 类abstractclassCompanyPluginExtension{abstractPropertyStringgetTeamName()abstractPropertyBooleangetEnableQualityGate()CompanyPluginExtension(){teamName.convention(default-team)enableQualityGate.convention(true)}}// 注册 Extension 并在插件中使用classCompanyPluginimplementsPluginProject{voidapply(Project project){defextensionproject.extensions.create(companyConfig,CompanyPluginExtension)project.tasks.register(printTeamInfo){doLast{printlnTeam:${extension.teamName.get()}printlnQuality Gate:${extension.enableQualityGate.get()}}}}}用户在build.gradle中companyConfig{teamNameplatform-teamenableQualityGatetrue}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 }使用 bundledependencies{testImplementation libs.bundles.testing}使用 catalog 中的 pluginplugins{alias(libs.plugins.spring.boot)}10. 构建缓存构建缓存复用历史 task 输出。开启org.gradle.cachingtrue执行时强制使用gradle clean build --build-cache查看缓存命中情况gradle build--info21|grep-EUP-TO-DATE|FROM-CACHE|cache自定义 task 缓存abstractclassGenerateBuildInfoextendsDefaultTask{// 必须声明 Input OutputFile 才能缓存InputabstractPropertyStringgetVersionName()OutputFileabstractRegularFilePropertygetOutputFile()TaskActionvoidgenerate(){outputFile.get().asFile.textversion${versionName.get()}}}// 标记为可缓存tasks.register(generateBuildInfo,GenerateBuildInfo){outputs.cacheIf{true}versionNameproject.version.toString()outputFilelayout.buildDirectory.file(generated/build-info.properties)}11. 配置缓存配置缓存复用配置阶段结果大型项目中能显著减少冷启动时间。开启org.gradle.configuration-cachetrue验证gradlehelp--configuration-cache常见不兼容原因问题解决方向task 持有Project对象改用Provider、Layout、ObjectFactory配置阶段读取文件改用providers.fileContents使用不可序列化对象改用 Gradle 提供的类型在配置阶段做网络请求移到 task 执行阶段12. 并行构建org.gradle.paralleltrue并行构建适合多模块项目。Gradle 会在依赖关系允许的情况下并行执行不同模块的 task。配合 worker API在单个 task 内并行abstractclassParallelProcessorextendsDefaultTask{InjectabstractWorkerExecutorgetWorkerExecutor()TaskActionvoidprocess(){defqueueworkerExecutor.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-librarymy-app/build.gradledependencies{// 坐标与 my-library 发布坐标一致Gradle 自动用本地版本implementationcom.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/checkoutv4-uses:actions/setup-javav4with:distribution:temurinjava-version:17-uses:gradle/actions/setup-gradlev4with:cache-read-only:${{github.ref!refs/heads/main}}-run:./gradlew clean build--stacktrace-name:Upload test reportsif:always()uses:actions/upload-artifactv4with:name:test-reportspath:**/build/reports/tests/关键 CI 配置缓存 Gradle User Homesetup-gradleaction 自动处理# CI 推荐命令./gradlew clean build--scan--stacktrace--no-daemon--no-daemon在短生命周期 CI 容器中避免 Daemon 残留。15. 实操验证 demo 的 Convention Plugincddemo/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# 查看所有 taskConvention Plugin 注册的 publish taskgradle :common:tasks验证点service/build.gradle非常精简Java 版本、编码、测试配置全来自 Convention Plugin。gradle :common:tasks能看到publish相关 taskConvention Plugin 添加的。16. 常见问题问题 1为什么不要在每个子模块复制同样配置复制配置短期快长期会导致版本不一致、升级困难、排查困难。超过 3 个模块共享同类配置时就应该考虑抽取 Convention Plugin。问题 2为什么修改 build-logic 后构建变慢构建逻辑本身也是代码。修改后需要重新编译插件并使相关配置缓存失效。构建逻辑应该稳定、清晰、少变。问题 3什么时候用 buildSrc什么时候用 build-logic场景推荐小项目少量构建工具类buildSrc多模块项目构建规范需要长期维护build-logic多仓库共享构建插件独立 Gradle 插件项目问题 4Provider 和直接读属性有什么区别直接读属性在配置阶段立即计算即使 task 从未执行也会触发。Provider 是懒加载只有真正需要值时才计算。大型项目中滥用立即计算会显著拖慢配置阶段。问题 5配置缓存和构建缓存有什么区别缓存类型复用内容作用构建缓存task 输出文件跳过已执行且输入未变的 task配置缓存配置阶段结果跳过整个配置阶段两者可以同时开启效果叠加。