本章目标
本章解决:当项目变大、模块变多、团队变多后,如何把 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
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
Sync 和 Copy 类似,但会删除目标目录中不在来源中的文件,保持目录"同步":
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.named 和 tasks.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 对象 |
改用 Provider、Layout、ObjectFactory |
| 配置阶段读取文件 | 改用 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 |
| 配置缓存 | 配置阶段结果 | 跳过整个配置阶段 |
两者可以同时开启,效果叠加。