Gradle 学习笔记
1. Gradle 是什么
Gradle 是一个开源的构建自动化工具。它根据构建脚本描述的规则,组织并执行编译、测试、代码生成、打包、发布和部署等工作。
Gradle 常用于:
- Java、Kotlin、Groovy、Scala 等 JVM 项目。
- Android 应用和 Android Library。
- 多模块或大型单体仓库。
- 原生、前端或跨语言工程中的统一任务编排。
- CI/CD 中的测试、打包、质量检查和发布。
Gradle 的核心价值不是“替代一条编译命令”,而是提供一套可扩展、可缓存、可组合、可诊断的构建模型。
1.1 Gradle 解决的问题
一个真实项目通常需要完成以下步骤:
读取配置 → 解析插件与依赖 → 生成源码或资源 → 编译 → 测试与检查 → 打包 → 发布如果这些步骤依赖人工执行,容易出现顺序错误、环境差异、依赖遗漏和不可复现。Gradle 将它们组织成任务图,并根据输入、输出和依赖关系决定执行顺序。
1.2 Gradle 的主要特点
- 声明式配置:描述项目、依赖、插件和任务关系。
- 可编程 DSL:可使用 Kotlin DSL 或 Groovy DSL 表达复杂逻辑。
- 插件化:Java、Kotlin、Android、发布等能力通过插件提供。
- 依赖管理:支持 Maven、Ivy、本地仓库和项目依赖。
- 增量构建:输入输出没有变化时跳过任务。
- 构建缓存:复用本机或其他机器生成的任务输出。
- 配置缓存:复用构建配置结果,减少后续启动成本。
- 多项目构建:管理多个相关模块和跨模块任务关系。
- Wrapper:固定 Gradle 版本,使开发机和 CI 使用相同环境。
1.3 Gradle 不只是“编译命令包装器”
编译器只负责把源代码转换成字节码或其他目标产物;依赖管理器主要负责查找和下载组件;CI 系统负责在指定环境中执行流水线。Gradle 的职责更宽,它把这些步骤组织成一个可观察、可组合的构建模型:
源码、资源、配置、依赖、工具链 ↓ Gradle 构建模型与任务图 ↓编译、测试、检查、代码生成、打包、发布和报告因此,Gradle 通常同时承担以下角色:
| 角色 | Gradle 中的对应能力 | 典型问题 |
|---|---|---|
| 构建编排器 | Task、Task Graph、Lifecycle | 哪些步骤执行,执行顺序是什么 |
| 依赖管理器 | Repository、Configuration、Variant | 依赖从哪里来,选择哪个版本 |
| 工具链协调器 | Java Toolchain、插件和外部工具 | 使用哪个 JDK、编译器或生成器 |
| 质量门禁 | test、check、Lint、覆盖率 | 代码是否满足交付条件 |
| 交付工具 | JAR、APK、AAB、Maven Publish | 如何生成、验证和发布构件 |
| 性能平台 | 增量构建、Build Cache、Configuration Cache | 如何减少重复工作 |
1.4 一个 Gradle 构建是怎样工作的
以执行 build 为例,可以把一次构建拆成以下步骤:
- 定位构建入口:Gradle 从当前目录寻找
settings.gradle.kts或settings.gradle。 - 初始化构建:确定根项目、子项目、插件管理和全局构建设置。
- 配置项目:应用插件,创建 Configuration、Source Set、Extension 和 Task。
- 解析请求:根据命令行选择
build需要的任务,并展开任务依赖。 - 解析依赖:针对具体 Configuration 选择仓库、版本和变体。
- 判断任务状态:检查任务输入、输出、依赖结果和缓存命中情况。
- 执行必要任务:编译、测试、检查和打包。
- 报告结果:输出日志、报告、构件和失败原因。
可以用下面的关系理解它:
用户命令 ↓任务选择 ↓任务依赖图 ↓输入与输出快照 ──→ up-to-date / cache hit ↓ ↓需要执行的任务 ←────── 复用已有结果 ↓构建产物与报告1.5 Gradle 的输入、规则与输出
任何可维护的构建都可以从三个问题开始:
- 输入是什么:源码、资源、依赖、编译参数、环境属性、工具链和版本。
- 规则是什么:插件约定、任务依赖、变体选择、版本约束和发布策略。
- 输出是什么:字节码、测试报告、覆盖率报告、JAR、APK、AAB 或发布元数据。
例如 Java 编译任务的抽象关系是:
Java 源码 + 编译参数 + JDK + 类路径 ↓ compileJava ↓ build/classes/java/main如果输入发生变化,输出可能需要重新生成;如果输入没有变化,Gradle 可以将任务标记为 UP-TO-DATE 或从 Build Cache 恢复。自定义任务是否正确声明输入和输出,直接决定了增量构建、缓存和并行执行是否可靠。
1.6 Gradle 与周边工具的关系
Gradle 不是孤立工作的。常见组合关系如下:
Gradle├── Java Toolchain:选择 JDK 和编译工具├── Kotlin Gradle Plugin:配置 Kotlin 编译├── Android Gradle Plugin:配置 Android 变体和打包├── JUnit、TestNG:执行测试├── Checkstyle、JaCoCo、Lint:质量检查和报告├── Maven/Ivy 仓库:解析和发布依赖├── GitHub Actions/GitLab CI:提供持续集成环境└── IDE:调用 Gradle 模型、任务和测试入口这也是为什么“Gradle 能不能使用某个版本”不能只看 Gradle 本身。实际兼容性还受 JDK、插件、Android Studio、Kotlin、操作系统和仓库环境影响。
1.7 Gradle 的优势与代价
优势
- 可以用统一模型覆盖从简单 Java 应用到大型多模块工程。
- 插件和 Convention Plugin 能把组织规范固化为可复用能力。
- 任务输入输出模型支持增量构建、缓存和并行执行。
- Kotlin DSL 提供较好的类型检查、补全和重构体验。
- 可同时支持本地开发、IDE、CI 和发布流程。
代价
- 学习成本高于单纯的脚本式构建工具。
- 插件、JDK、语言和框架之间存在兼容矩阵。
- 动态构建逻辑容易造成配置阶段变慢或行为隐式。
- 依赖变体、传递依赖和缓存问题需要系统化诊断。
- 构建脚本本身也是代码,需要测试、审查和版本治理。
不要只因为 Gradle 功能强大就把所有逻辑都放进构建脚本。构建逻辑应保持可读、可验证,并把复杂业务能力放到合适的独立工具或服务中。
** 哪些场景适合使用 Gradle**
Gradle 特别适合以下场景:
- 项目需要多个编译、测试、生成和发布步骤协同执行。
- 依赖关系复杂,且需要处理传递依赖、平台和变体。
- 仓库包含多个相互依赖的模块。
- 团队希望通过缓存和增量构建缩短反馈时间。
- 需要将本地构建、IDE 和 CI 的命令统一起来。
- 需要构建 Java/Kotlin Library、Android 应用或可发布构件。
哪些场景不应过度使用 Gradle
以下场景不一定需要引入复杂的 Gradle 逻辑:
- 只有一条固定编译命令且没有依赖管理需求的小型实验。
- 一个简单脚本即可完成的文件复制或静态资源处理。
- 已经稳定运行、团队也不需要扩展的 Maven 项目。
- 需要复杂部署编排、审批和运行时状态管理的系统。
Gradle 可以调用外部工具,但它不是通用工作流平台、应用运行时或部署控制面。构建脚本应负责“构建和交付输入”,不应替代业务系统和生产运维平台。
2. 核心术语与心智模型
2.1 Build
一次 Gradle Build 是 Gradle 对一个构建定义进行初始化、配置和执行的完整过程。一个 Build 可以包含一个项目,也可以包含多个项目。
2.2 Project
Project 是可构建的软件单元,例如 Java 应用、Java Library、Android App 模块、Android Library 模块或只负责聚合任务的根项目。多项目构建中通常存在一个根项目和多个子项目。
2.3 Task
Task 是 Gradle 的基本工作单元,例如:
compileJava:编译 Java 源码。test:运行测试。jar:生成 JAR。build:完成构建和验证。publish:发布构件。assembleDebug:生成 Android Debug 产物。
Task 可以依赖其他 Task。Gradle 根据依赖关系构建有向无环图,再执行用户请求所需的节点。
2.4 Plugin
Plugin 用来给项目添加能力、任务、扩展对象和约定:
plugins { java application `maven-publish`}应用 java 插件后,项目会获得标准 Source Set、编译任务、测试任务和 JAR 任务。
2.5 Dependency
Dependency 是项目在编译、运行、测试或构建过程中需要的外部或内部组件:
dependencies { implementation("com.google.guava:guava:33.5.0-jre") testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")}依赖不只包含 JAR。Gradle 还会处理依赖元数据、传递依赖、能力、属性和变体。
2.6 Configuration
Configuration 是一组具有特定用途的依赖集合或可发布变体,例如 implementation、api、compileClasspath、runtimeClasspath 和 testImplementation。
不要把 Configuration 简单理解成“依赖列表”。它同时承担声明、解析和变体选择等角色。
2.7 Build Script 与 Settings
构建脚本用于声明插件、依赖、任务和项目配置:
build.gradle.kts Kotlin DSLbuild.gradle Groovy DSLSettings 负责定义整个构建的边界和全局结构,常见职责包括根项目名称、子项目、插件仓库、依赖仓库策略、Version Catalog、Composite Build 和 Build Cache。
2.8 一句话心智模型
Settings 决定“构建里有什么”Project 决定“每个模块怎么构建”Plugin 提供“可用能力与约定”Task 表示“需要完成的工作”Dependency 提供“构建需要的组件”2.9 Gradle 的对象关系
Gradle 的核心对象不是彼此孤立的,可以用下面的关系图理解:
Gradle Build├── Settings│ ├── rootProject│ ├── included projects│ ├── pluginManagement│ ├── dependencyResolutionManagement│ └── included builds└── Projects ├── plugins ├── extensions ├── configurations ├── source sets ├── dependencies └── tasks一个 Project 中的 Plugin 通常会创建或配置其他对象:
应用 java 插件 ↓创建 Source Set、Configuration、Extension 和标准 Task ↓声明依赖、配置编译器和测试平台 ↓形成 compileJava → classes → test → check → build 的关系这说明很多 Gradle 配置并不是独立开关,而是对对象模型中的某个对象进行配置。遇到配置项时,先问它属于 Settings、Project、Plugin、Extension、Configuration 还是 Task,通常更容易找到正确写法。
2.10 Build、Project 与 Settings 的边界
| 对象 | 作用范围 | 典型配置 |
|---|---|---|
Gradle | 一次构建进程 | 生命周期监听、构建启动参数、全局服务 |
Settings | 整个构建 | 项目包含关系、插件仓库、依赖仓库、Version Catalog |
Project | 一个模块 | 插件、依赖、任务、编译和发布配置 |
Task | 一个工作单元 | 输入、输出、动作、依赖和执行条件 |
一个常见错误是把 Project 级配置放到 Settings,或把任务执行逻辑直接放到 Settings。判断位置时可以使用这个原则:
- 决定“有哪些项目”的配置放在 Settings。
- 决定“某个项目如何构建”的配置放在 Project。
- 决定“某一步如何执行”的配置放在 Task 或自定义 Task 类型。
- 决定“多个项目如何统一”的逻辑放在 Convention Plugin。
2.11 Task 图:依赖关系不是脚本顺序
执行以下命令:
.\gradlew.bat buildGradle 不会简单地从上到下执行构建脚本中的每一行,而是根据用户请求和任务依赖构造任务图。一个简化的 Java 构建图可能是:
compileJava ──→ classes ──→ jar ──→ assemble │ │ └────→ test ──→ check ────┘ ↓ build其中箭头表示“必须先完成”。build 是生命周期任务,通常通过依赖关系聚合编译、测试、检查和打包任务;它本身不一定包含大量实际动作。
查看任务图的常用方式:
.\gradlew.bat build --dry-run.\gradlew.bat tasks --all.\gradlew.bat build --info--dry-run 只展示将执行的任务,不执行任务动作;--info 可以帮助观察任务跳过、依赖解析和执行原因。
2.12 dependsOn、mustRunAfter 与 finalizedBy
这三个 API 表达的关系不同:
taskB { dependsOn(taskA)}
taskC { mustRunAfter(taskA)}
taskD { finalizedBy(taskE)}dependsOn:B 需要 A,执行 B 时会把 A 加入任务图。mustRunAfter:A 和 C 都在任务图中时,C 必须排在 A 后面;它不会自动加入 A。finalizedBy:任务结束后执行终结任务,常用于清理或生成辅助报告。
如果任务 B 真正读取任务 A 的输出,优先使用 dependsOn 和文件 Provider 表达关系,而不是只写 mustRunAfter。只有顺序相关但没有数据依赖时,才使用顺序规则。
2.13 Task 配置与 Task 执行
下面两段代码发生在不同阶段:
tasks.register("demo") { group = "example" description = "示例任务"
doLast { println("任务动作执行") }}group和description是任务配置,通常在 Configuration 阶段处理。doLast中的代码是任务动作,在 Execution 阶段处理。
不要在任务配置阶段读取大量文件、访问网络或运行外部程序。应把这些行为放进任务动作,并把相关文件、参数和环境声明为输入,使 Gradle 能够判断任务是否需要执行。
2.14 声明式配置与命令式逻辑
Gradle DSL 看起来像普通编程语言,但推荐的构建脚本仍应尽可能表达“目标状态”和“对象关系”:
tasks.withType<Test>().configureEach { useJUnitPlatform()}这比遍历所有任务、判断名称并立即修改对象更容易维护:
tasks.all { if (name.endsWith("Test")) { // 隐式匹配,容易受到插件变化影响 }}命令式逻辑并非完全不能使用,但应限制在自定义插件或清晰封装的规则中,并避免依赖任务创建顺序、项目评估顺序或全局可变状态。
2.15 Dependency 与 Task Dependency 不是一回事
这两个概念都叫“依赖”,但层次不同:
| 类型 | 说明 | 示例 |
|---|---|---|
| Task dependency | 一个任务依赖另一个任务的执行结果 | build 依赖 test |
| Project dependency | 一个模块依赖另一个模块的构件 | app 依赖 project(":core") |
| External dependency | 项目依赖仓库中的外部模块 | implementation("group:name:version") |
| Transitive dependency | 依赖的依赖被继续解析 | A → B → C |
| Dependency constraint | 对版本或选择结果施加约束 | strictly("2.0") |
例如:
dependencies { implementation(project(":core")) implementation("com.google.guava:guava:33.5.0-jre")}这段代码声明的是 Project 和 External Dependency,不等于声明某个 Task 的执行顺序。项目依赖会影响类路径和变体选择,也可能间接影响任务图,但两者不能互相替代。
2.16 Configuration 的三种角色
现代 Gradle 中,Configuration 通常可以从用途上分为三类:
- Declarable:用于声明依赖,例如
implementation。 - Resolvable:用于解析文件,例如
runtimeClasspath。 - Consumable:用于向其他项目提供变体,例如
runtimeElements。
可以用一个简化例子表示:
implementation ──继承或参与──→ runtimeClasspath ↓ 解析得到文件
java-library 项目 ──发布──→ apiElements / runtimeElements不要随意把一个声明 Configuration 当作文件集合直接解析,也不要把所有依赖都塞进 compileClasspath。使用插件提供的标准 Configuration,通常比手工拼接类路径更安全。
2.17 Extension 与 Convention
Plugin 往往通过 Extension 暴露配置入口:
application { mainClass = "com.example.App"}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}这里的 application {} 和 java {} 不是普通函数,而是插件创建的 Extension 或类型安全访问器。它们把用户配置传递给插件,再由插件创建或配置任务。
Convention 是团队对这些配置的默认约定,例如所有 Java Library 都使用 JDK 21、JUnit Platform 和统一的编译参数。Convention 应通过插件实现,而不是在根项目中隐式修改所有子项目。
2.18 项目路径与任务路径
多项目构建中,项目和任务都有路径:
: 根项目:app app 子项目:app:test app 项目的 test 任务:core:compileJava core 项目的 Java 编译任务执行指定模块任务:
.\gradlew.bat :app:test.\gradlew.bat :core:compileJava在脚本中引用跨项目任务时,应尽量使用 Provider 和类型安全 API;不要通过字符串拼接大量任务名称来隐藏模块关系。
3. Gradle、Maven 与 Ant
3.1 Ant
Ant 更接近命令式任务编排。开发者显式描述每一步执行什么、先后顺序是什么。它灵活,但大型构建容易出现重复 XML、手工任务关系和维护成本。
3.2 Maven
Maven 使用 XML 和固定生命周期,强调约定优于配置。它的目录约定、生命周期和企业生态成熟,但复杂定制通常需要插件或冗长 XML。
3.3 Gradle
Gradle 吸收了 Ant 的灵活性和 Maven 的依赖、仓库、约定思想,并提供任务图、插件、增量构建和缓存机制。
3.4 如何选择
- 维护既有 Maven 项目:尊重现有工程,不要仅为“新”而迁移。
- Android:使用 Gradle 和 Android Gradle Plugin。
- 新建复杂 JVM、多模块或需要高度定制的项目:Gradle 通常更合适。
- 简单、稳定且团队更熟悉 Maven 的项目:Maven 仍然合理。
构建工具没有绝对优劣,真正重要的是可复现、可维护、可升级和团队可理解。
3.5 三种构建工具的核心模型
| 维度 | Ant | Maven | Gradle |
|---|---|---|---|
| 核心抽象 | Target、Task、Property | Project、Lifecycle、Phase、Plugin | Build、Project、Task、Plugin、Configuration |
| 配置形式 | XML 命令式描述 | XML 声明项目模型 | Kotlin/Groovy DSL 与对象模型 |
| 默认约定 | 较少,开发者自行组织 | 强调约定优于配置 | 通过插件和约定提供默认行为 |
| 生命周期 | Target 依赖关系 | 固定阶段和阶段绑定 | 任务图和插件提供的生命周期任务 |
| 依赖管理 | 依赖外部工具或 Ivy | Maven Repository、POM | Maven/Ivy Repository、Module Metadata、Variant |
| 多项目 | 需要自行编排 | Reactor 构建 | 原生多项目与 Composite Build |
| 性能机制 | 主要依赖任务设计 | 有增量和并行能力,但表达方式更固定 | 增量构建、Build Cache、Configuration Cache |
| 扩展方式 | 自定义 Task、TaskDef | Plugin、Lifecycle、Mojo | Plugin、Extension、Custom Task、Build Service |
| 类型安全 | XML 结构约束有限 | XML schema 和插件约定 | Kotlin DSL 可提供较强类型检查 |
| 典型优势 | 简单、直接、兼容历史工具 | 约定稳定、生态成熟 | 灵活、可组合、适合复杂工程 |
表格只能帮助建立方向,不能替代对现有仓库的评估。一个成熟的 Maven 项目可能比一个缺少约定的 Gradle 项目更容易维护;一个简单的 Ant 构建也可能比过度抽象的 Gradle 构建更容易排错。
3.6 Ant 的工作方式
Ant 构建通常通过 build.xml 定义 Property、Target 和 Task:
<project name="demo" default="build" basedir="."> <property name="build.dir" value="build"/>
<target name="compile"> <mkdir dir="${build.dir}/classes"/> <javac srcdir="src" destdir="${build.dir}/classes"/> </target>
<target name="build" depends="compile"> <jar destfile="${build.dir}/demo.jar" basedir="${build.dir}/classes"/> </target></project>Ant 的特点是开发者直接描述动作:创建目录、调用编译器、复制文件、生成压缩包。depends 建立 Target 之间的先后关系,但 Ant 不会自动理解所有输入输出,也不会自动为每个任务建立现代缓存模型。
适合 Ant 的场景:
- 维护已有的 XML 构建流程。
- 需要直接调用大量历史 Ant Task。
- 构建步骤简单、团队熟悉 XML 且没有复杂依赖治理需求。
Ant 的主要维护风险:
- Target 之间的依赖容易变成隐式顺序。
- 相同路径、参数和动作可能在多个 Target 中重复。
- 构建逻辑增长后,XML 层级和 Property 覆盖关系难以追踪。
- 依赖下载、版本冲突和发布元数据通常需要额外工具配合。
3.7 Maven 的工作方式
Maven 项目通常由 pom.xml 描述:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>demo</artifactId> <version>1.0.0</version>
<properties> <maven.compiler.release>21</maven.compiler.release> </properties>
<dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>6.0.3</version> <scope>test</scope> </dependency> </dependencies></project>Maven 的核心是 Project Object Model(POM)和标准生命周期。用户执行 mvn package 时,Maven 会沿生命周期执行从验证、编译、测试到打包所需的阶段;插件将具体 Mojo 绑定到生命周期阶段。
Maven 的优势:
- 标准目录和生命周期约定清晰。
- POM、仓库和发布生态成熟。
- 团队成员通常可以快速理解基础项目。
- 多模块 Reactor 能够统一构建相关模块。
Maven 的边界:
- 大量非标准流程通常需要编写或组合插件。
- XML 表达复杂条件、循环和对象关系比较笨重。
- 生命周期阶段固定,特殊任务编排需要理解插件绑定和执行顺序。
- 性能优化和自定义构建模型的表达空间相对受限。
3.8 Gradle 的工作方式
Gradle 通过 Settings、Project、Plugin 和 Task 组成构建模型:
plugins { java}
repositories { mavenCentral()}
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")}
tasks.withType<Test>().configureEach { useJUnitPlatform()}Gradle 不要求所有构建都遵循一个固定生命周期,而是由插件创建任务、任务之间建立依赖,用户请求某个生命周期任务时再计算需要执行的任务图。
这种模型带来更大的表达能力,也带来更高的设计责任:
- 要明确任务输入和输出。
- 要避免配置阶段副作用。
- 要控制跨项目隐式配置。
- 要治理插件和依赖版本。
- 要持续验证 Configuration Cache、Build Cache 和并行构建的兼容性。
3.9 生命周期对比
| 需求 | Ant | Maven | Gradle |
|---|---|---|---|
| 定义构建入口 | 默认 Target | 默认生命周期阶段 | 生命周期 Task 或自定义聚合 Task |
| 表达先后关系 | Target depends | Phase 顺序和 Plugin binding | dependsOn、输入输出和顺序规则 |
| 运行单个步骤 | Target | Phase 或 Plugin Goal | Task |
| 跳过任务 | 条件和 Target 设计 | Profile、参数、插件配置 | -x、onlyIf、任务条件 |
| 查看执行路径 | 日志和 XML | -X、插件日志 | --dry-run、--info、Build Scan |
| 复用构建逻辑 | XML Entity、宏和自定义 Task | Parent POM、Plugin | Convention Plugin、Included Build |
不要把 Maven 的 Phase 名称机械映射成 Gradle Task 名称。例如 Maven 的 package 和 Gradle 的 build 都可能生成构件,但它们的验证范围、插件绑定和任务关系并不完全相同。迁移时应先列出实际交付目标,再映射任务和验证门禁。
3.10 依赖管理对比
Ant
Ant 本身不提供与 Maven/Gradle 同等完整的现代依赖解析模型。项目通常通过 Ivy、手工下载、仓库插件或脚本获取 JAR,再将它们拼入编译和运行类路径。
Maven
Maven 使用 POM 坐标、Scope、传递依赖和仓库解析:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>33.5.0-jre</version></dependency>它强调固定坐标和标准范围,例如 compile、runtime、test、provided 等。复杂版本治理通常通过 Dependency Management、BOM、Parent POM 和 Enforcer Plugin 完成。
Gradle
Gradle 使用 Configuration 表达依赖用途,并可进一步处理属性、Variant、Capability、Platform、Constraint、锁定和验证:
dependencies { implementation("com.google.guava:guava:33.5.0-jre") testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")}Gradle 的模型更细,但也更容易出现“声明成功、解析失败”或“解析成功、运行时不兼容”。排查时不能只看依赖坐标,还要看 Configuration、变体和最终类路径。
3.11 插件和扩展机制对比
| 场景 | Ant | Maven | Gradle |
|---|---|---|---|
| 添加一个编译步骤 | 自定义 Target/Task | Plugin 和 Mojo | Plugin 和 Task |
| 添加配置入口 | Property | POM 配置、Plugin Parameter | Extension、Property、Provider |
| 复用团队规范 | XML 模板或脚本 | Parent POM、公司 Plugin | Convention Plugin |
| 共享服务 | 外部脚本或 Task | Plugin 级实现 | Build Service、Worker API |
| 处理不同项目变体 | 条件 Target | Profile | Variant、Attribute、Provider |
Gradle Extension 的优势是可以将配置和实现分离:
companyQuality { failOnWarning.set(true) reportDirectory.set(layout.buildDirectory.dir("reports/company"))}但扩展对象越多,构建模型越复杂。插件作者应提供合理默认值、明确属性类型和错误信息,不要让消费者依赖内部 Task 名称。
3.12 多模块项目对比
Maven 常用 Parent POM 和 <modules> 管理多模块:
root├── pom.xml├── core/pom.xml└── app/pom.xmlGradle 使用 Settings 声明项目:
rootProject.name = "demo"include("core", "app")Ant 通常需要自行编排多个子目录中的 Target。Gradle 和 Maven 都可以建立模块依赖,但 Gradle 还可以使用 Composite Build 将多个独立构建组合起来:
includeBuild("build-logic")选择多模块方案时,重点不是目录数量,而是:
- 模块是否有稳定职责。
- 依赖方向是否清晰。
- 是否需要独立测试、发布和版本。
- 共享构建逻辑是否已经成为维护负担。
- 构建性能是否因模块数量和配置方式受到影响。
3.13 可复现性与性能对比
| 能力 | Ant | Maven | Gradle |
|---|---|---|---|
| 固定工具版本 | 需要外部脚本或环境管理 | Maven Wrapper | Gradle Wrapper |
| 依赖版本治理 | 依赖 Ivy 或外部方案 | Dependency Management、BOM | Version Catalog、Platform、Constraint、Locking |
| 增量执行 | 依赖 Target 设计 | 插件和任务能力 | 任务输入输出模型 |
| 本地缓存 | 较少内置约定 | 可通过插件和配置增强 | Build Cache、Daemon |
| 配置缓存 | 无统一模型 | 受 Maven 生命周期影响 | Configuration Cache |
| 性能诊断 | 日志和外部工具 | Debug 日志和插件报告 | Profile、Build Scan、任务状态 |
无论使用哪一种工具,都应固定:
- 构建工具版本。
- JDK 和编译器版本。
- 依赖版本和仓库来源。
- 操作系统差异和环境变量。
- 生成物、报告和失败日志。
性能也不能只比较一次 clean build 的耗时。应区分首次构建、增量构建、CI 干净构建、缓存命中和依赖已下载等场景。
3.14 Maven 项目迁移到 Gradle
迁移不应从“把 XML 翻译成 Kotlin”开始,而应从构建契约开始:
- 列出源码目录、资源目录、测试目录和生成目录。
- 记录所有生命周期阶段、插件 Goal 和自定义脚本。
- 记录直接依赖、传递依赖、BOM、Profile 和仓库。
- 记录发布的 POM、Module Metadata、sources、javadoc 和签名要求。
- 使用
gradle init或迁移工具生成初始结构。 - 先让编译和测试通过,再迁移质量检查、代码生成和发布。
- 比较迁移前后的类路径、测试结果和构件内容。
- 删除旧构建入口前,在 CI 和干净环境中验证一段时间。
常见迁移映射:
| Maven | Gradle |
|---|---|
pom.xml | settings.gradle.kts + build.gradle.kts |
groupId | group |
artifactId | Project name 或发布配置 |
version | version |
dependency | dependencies {} |
scope=test | testImplementation |
scope=provided | compileOnly 或插件特定 Configuration |
maven-surefire-plugin | Test 任务配置 |
maven-jar-plugin | jar 任务配置 |
maven-publish/Deploy Plugin | maven-publish Plugin |
| Parent POM | Convention Plugin、Platform 或 Version Catalog |
| Maven Profile | Gradle Property、Variant 或独立任务 |
映射表只是起点。Maven Profile 可能同时包含属性、依赖、插件和资源替换,迁移时应拆成多个职责明确的 Gradle 配置。
3.15 Ant 构建迁移到 Gradle
Ant 迁移应先识别每个 Target 的输入、输出和真实依赖:
Target compile├── 输入:src/**/*.java、编译参数、外部 JAR├── 输出:build/classes└── 后续:test、jar再将其转换为 Gradle Task 或标准插件配置:
javac→java插件提供的compileJava。junit→Test任务和测试依赖。jar→jar任务。copy→Copy任务或 Source Set 资源配置。exec→ 明确声明输入输出的自定义 Task。- 公共 XML Target → Convention Plugin。
Gradle 可以调用 Ant Task 以降低迁移风险:
tasks.register("legacyAntStep") { doLast { ant.invokeMethod("echo", mapOf("message" to "legacy step")) }}但兼容调用应是过渡方案。长期目标应是把核心流程转换成 Gradle 原生插件、任务类型和输入输出模型。
3.16 选择决策矩阵
| 问题 | 倾向 Ant | 倾向 Maven | 倾向 Gradle |
|---|---|---|---|
| 是否已有大量稳定 XML Target | 是 | 否 | 谨慎迁移 |
| 是否重视固定生命周期和标准目录 | 否 | 是 | 也可以,但需自定义 |
| 是否需要复杂任务编排 | 可以但维护成本上升 | 需要插件 | 是 |
| 是否需要多模块和变体模型 | 需要自行实现 | 中等 | 是 |
| 是否需要缓存和增量优化 | 依赖设计 | 取决于插件 | 是 |
| 团队是否熟悉 XML/POM | Ant/Maven | Maven | Gradle |
| 是否需要 Android 构建 | 否 | 否 | 是 |
| 是否已有企业 Maven 规范 | 继续 Maven | 是 | 迁移成本较高 |
最终选择应基于总拥有成本,包括学习、迁移、插件维护、CI、升级、排错和团队交接成本。
4. 环境准备与安装
4.1 是否必须全局安装 Gradle
运行已有项目时通常不需要全局安装 Gradle,应优先使用项目中的 Wrapper:
.\gradlew.bat build./gradlew build只有创建全新 Wrapper、维护特殊环境或学习 Gradle 本身时,才可能需要全局 Gradle。
4.2 Java 运行环境
Gradle 本身运行在 JVM 上。Gradle 9.6.1 支持使用 JVM 17 至 JVM 26 运行。本文建议学习环境使用 JDK 21;真实项目应按 Gradle、框架、Android Gradle Plugin 和部署目标的兼容矩阵选择版本。
检查环境:
java -version$env:JAVA_HOME.\gradlew.bat --versionJAVA_HOME、IDE 的 Gradle JVM、Java Toolchain 和应用运行 JDK 是不同概念。排错时要确认究竟是哪一个 JVM 在工作。
4.3 常见安装方式
可以通过 SDKMAN!、Homebrew、Scoop 或手动下载二进制包安装全局 Gradle。安装后检查:
gradle --version不要因为系统中存在全局 Gradle 就跳过 Wrapper。项目构建仍应优先调用 gradlew 或 gradlew.bat。
4.4 环境准备清单
开始使用 Gradle 前,至少确认以下条件:
- 操作系统、Shell 和工作目录明确。
- 用于启动 Gradle 的 JVM 在当前 Gradle 版本支持范围内。
- 项目是否已经包含 Wrapper。
JAVA_HOME、IDE Gradle JVM 和 Java Toolchain 是否符合预期。- 网络、代理、证书和私有仓库凭据可用。
- 当前用户对项目目录、Gradle User Home 和构建输出目录有读写权限。
- Windows 上的脚本执行策略、Linux/macOS 上的 Wrapper 可执行权限正常。
建议在问题出现前记录一份环境基线:
OS:Windows / Linux / macOS + 版本Shell:PowerShell / Bash / zshGradle:Wrapper 或全局版本JDK:版本、供应商、JAVA_HOMEIDE:名称和 Gradle JVM项目:Gradle、Kotlin、AGP 或其他插件版本网络:直连、代理或私有仓库环境信息越完整,越容易区分“构建脚本错误”和“本机环境错误”。
4.5 安装全局 Gradle 的方式
已有项目优先使用 Wrapper;全局安装主要用于创建 Wrapper、初始化新项目或学习 Gradle 命令本身。
Windows
可以使用 Scoop:
scoop install gradle也可以从 Gradle 官方发布页下载压缩包,解压到稳定目录,例如:
C:\Tools\gradle\gradle-9.6.1再配置:
$env:GRADLE_HOME = 'C:\Tools\gradle\gradle-9.6.1'$env:Path = "$env:GRADLE_HOME\bin;$env:Path"gradle --version用户级环境变量应通过系统设置或 PowerShell Profile 持久化,不要只依赖当前终端窗口中的临时变量。
macOS
可以使用 Homebrew:
brew install gradle或使用 SDKMAN!:
curl -s "https://get.sdkman.io" | bashsdk install gradle 9.6.1Linux
SDKMAN! 适合在多个 JDK 和 Gradle 版本之间切换:
curl -s "https://get.sdkman.io" | bashsdk install gradle 9.6.1也可以手动下载并解压 Gradle 分发包,再将 bin 目录加入 PATH。系统包管理器中的版本可能滞后于官方版本,生产构建仍应依赖项目 Wrapper。
4.6 验证全局安装
Windows PowerShell:
Get-Command gradleGet-Command javajava -versiongradle --versionLinux/macOS:
command -v gradlecommand -v javajava -versiongradle --version重点观察 gradle --version 输出中的:
- Gradle 版本。
- Gradle Launcher JVM。
- Daemon JVM。
- 操作系统和架构。
- Kotlin、Groovy、Ant 运行时版本。
如果终端中显示的 Gradle 不是预期版本,通常是 PATH 中存在多个安装目录。Windows 可使用 where.exe gradle,Linux/macOS 可使用 which -a gradle 检查所有候选路径。
4.7 四个容易混淆的 Java 环境
Gradle 项目中至少可能出现四种 JVM:
| JVM | 作用 | 常见配置来源 |
|---|---|---|
| Gradle Launcher JVM | 启动 Gradle 客户端进程 | JAVA_HOME、系统 PATH |
| Gradle Daemon JVM | 执行配置和任务 | JAVA_HOME、org.gradle.java.home |
| Java Toolchain JVM | 编译、测试或运行任务 | java.toolchain、kotlin.jvmToolchain |
| 应用运行 JVM | 执行最终 Java 应用 | 启动脚本、容器或部署环境 |
它们可以是同一个 JDK,也可以不同。例如:
Gradle 用 JDK 21 启动Java 编译 Toolchain 使用 JDK 17应用在运行环境使用 JDK 21排查版本错误时不要只执行 java -version。应同时检查:
java -version.\gradlew.bat --version.\gradlew.bat properties4.8 JAVA_HOME、org.gradle.java.home 与 Toolchain
JAVA_HOME 是操作系统和许多命令行工具默认使用的 JDK 位置:
$env:JAVA_HOMEorg.gradle.java.home 可以在 gradle.properties 中指定 Gradle 使用的 Java Home:
org.gradle.java.home=C:/Program Files/Java/jdk-21不要把机器绝对路径提交到团队公共配置中。个人环境可以放在用户级 gradle.properties,CI 应通过执行器镜像、环境变量或安全配置提供。
Java Toolchain 用于声明项目任务需要的 Java 版本:
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}三者的职责不同:
JAVA_HOME:默认启动环境org.gradle.java.home:Gradle 进程的特定 JVMToolchain:项目编译、测试和运行任务的目标 JVM4.9 Gradle User Home 与项目目录
Gradle User Home 默认位置:
Windows:%USERPROFILE%\\.gradleLinux/macOS:~/.gradle可以通过环境变量修改:
$env:GRADLE_USER_HOME = 'D:\gradle-user-home'export GRADLE_USER_HOME="$HOME/.gradle-custom"Gradle User Home 可能包含:
- Wrapper 下载的 Gradle 分发包。
- 依赖和模块元数据缓存。
- Daemon 状态和日志。
- 用户级
gradle.properties。 - 用户级初始化脚本。
- 文件系统 watching 和构建缓存数据。
项目目录中的 .gradle/ 是项目级缓存,build/ 是项目构建输出。它们通常不应提交到版本控制:
.gradle/build/清理 User Home 之前应先停止 Daemon,并确认不会误删企业证书、凭据或其他工具配置:
.\gradlew.bat --stop4.10 网络、代理与私有仓库
依赖下载失败不一定是 Gradle 版本问题,常见原因包括代理、TLS 证书、DNS、仓库权限和镜像不可用。
可以在用户级或项目级 gradle.properties 中配置代理:
systemProp.http.proxyHost=proxy.example.comsystemProp.http.proxyPort=8080systemProp.https.proxyHost=proxy.example.comsystemProp.https.proxyPort=8080不要把代理密码和仓库密码提交到公共项目。更推荐使用环境变量、用户级属性或 CI Secret:
systemProp.https.proxyUser=${HTTPS_PROXY_USER}systemProp.https.proxyPassword=${HTTPS_PROXY_PASSWORD}实际使用时确认凭据注入方式符合 Gradle 和 CI 平台的属性解析规则,不要把密码直接写入日志。
网络排查顺序:
- 用
java -version和gradle --version确认环境。 - 检查仓库 URL、代理和证书。
- 用
dependencies --info观察请求的仓库和失败原因。 - 确认私有仓库凭据和访问权限。
- 最后再使用
--refresh-dependencies验证缓存是否过期。
4.11 Windows、Linux 与 macOS 差异
Windows
- PowerShell 调用 Wrapper 使用
.\gradlew.bat,不要写成 Bash 的./gradlew习惯。 - 使用
where.exe java和where.exe gradle检查 PATH 冲突。 - 注意路径中的空格、反斜杠和 Windows 服务账户权限。
- 运行 CI 或脚本时确认执行账户拥有 Gradle User Home 权限。
Linux/macOS
- Wrapper 首次使用前确认可执行权限:
chmod +x gradlew./gradlew --version- 使用
which -a java、which -a gradle检查多版本冲突。 - 注意文件名大小写、符号链接和挂载目录权限。
- 容器中应明确 JDK、用户、工作目录和 Gradle User Home。
跨平台项目
- 命令示例同时提供 PowerShell 和 Bash 版本。
- 构建脚本使用
layout、FileSystemOperations等 Gradle API,不要硬编码本机路径。 - 通过 Toolchain 固定语言版本,通过 Provider 读取环境差异。
- 不要把 Windows 路径分隔符写入发布元数据或源码生成结果。
4.12 IDE 中的 Gradle 环境
IDE 可能有独立的 Gradle JVM 配置,不一定等于终端中的 JAVA_HOME。出现“终端能构建、IDE 不能构建”时检查:
- IDE 使用的是 Wrapper 还是本机 Gradle。
- IDE Gradle JVM 的版本和路径。
- IDE 是否覆盖了项目的
gradle.properties。 - IDE 使用的代理、证书和凭据是否不同。
- 项目同步失败是配置阶段问题,还是任务执行问题。
建议先用命令行 Wrapper 验证:
.\gradlew.bat help.\gradlew.bat tasks如果命令行成功而 IDE 失败,优先检查 IDE 的 Gradle 设置;如果两者都失败,再检查项目脚本、插件和依赖。
4.13 CI 中的环境准备
CI 不应依赖执行器预装的全局 Gradle。推荐流程:
检出代码 ↓准备明确版本的 JDK ↓调用项目 Wrapper ↓恢复安全缓存 ↓执行 check/build ↓保存测试报告和构建产物CI 环境至少要明确:
- JDK 发行版和版本。
- 操作系统架构。
- Wrapper 分发包来源和校验。
- Gradle User Home 缓存位置。
- 私有仓库、代理和证书。
- 发布凭据是否只在受保护任务中可用。
5. Gradle Wrapper
Wrapper 是随项目提交的一组启动脚本和配置文件,用于下载并运行项目指定的 Gradle 版本。
5.1 Wrapper 文件
gradle/└── wrapper/ ├── gradle-wrapper.jar └── gradle-wrapper.propertiesgradlewgradlew.bat这些文件通常都应提交到版本控制。
5.2 Wrapper 的价值
- 团队成员使用同一个 Gradle 版本。
- CI 不需要预装特定 Gradle。
- 升级记录进入版本控制。
- 减少“我的机器可以构建”的环境差异。
5.3 生成和使用 Wrapper
gradle wrapper --gradle-version 9.6.1 --distribution-type allWindows PowerShell:
.\gradlew.bat tasks.\gradlew.bat buildLinux/macOS:
./gradlew tasks./gradlew buildall 分发包包含二进制、源码和文档,IDE 体验更完整;CI 若只追求体积可考虑 bin。
5.4 Wrapper 配置
gradle/wrapper/gradle-wrapper.properties 示例:
distributionBase=GRADLE_USER_HOMEdistributionPath=wrapper/distsdistributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-all.zipnetworkTimeout=10000validateDistributionUrl=truezipStoreBase=GRADLE_USER_HOMEzipStorePath=wrapper/dists5.5 升级 Wrapper
.\gradlew.bat wrapper --gradle-version 9.6.1 --distribution-type all.\gradlew.bat wrapper.\gradlew.bat --version.\gradlew.bat help --warning-mode all.\gradlew.bat build再次运行 Wrapper 任务,可以使启动脚本、Wrapper JAR 和属性文件都由目标版本生成。
5.6 Wrapper 安全
- 不随意接受来源不明的
gradle-wrapper.jar。 - 审查
distributionUrl是否指向可信来源。 - 使用
distributionSha256Sum固定分发包校验值。 - 代码审查中关注 Wrapper 版本变化。
- CI 中避免对不可信分支授予发布凭据或高权限。
5.7 Wrapper 的执行链路
执行:
.\gradlew.bat build可以抽象为以下过程:
gradlew.bat / gradlew ↓读取 gradle-wrapper.properties ↓定位 GRADLE_USER_HOME ↓检查目标 Gradle 分发包是否已缓存 ↓必要时下载并校验分发包 ↓启动指定版本的 Gradle ↓执行 build 任务Wrapper 并不是把 Gradle 完整复制进 Git 仓库,而是提交启动代码、配置和 Wrapper JAR;实际 Gradle 分发包通常下载到用户级缓存目录。这样可以保持仓库体积较小,并让不同项目使用不同 Gradle 版本。
5.8 Wrapper 文件职责
| 文件 | 作用 | 是否通常提交 |
|---|---|---|
gradlew | Linux/macOS 启动脚本 | 是 |
gradlew.bat | Windows 启动脚本 | 是 |
gradle-wrapper.jar | Wrapper 启动实现 | 是 |
gradle-wrapper.properties | 分发包 URL、缓存位置和网络配置 | 是 |
gradle-wrapper-validation.yml | CI 中验证 Wrapper 的工作流 | 可选但推荐 |
GRADLE_USER_HOME 下的分发包 | 本机下载缓存 | 否 |
不要只提交 gradlew 而遗漏 gradle-wrapper.jar 或 gradle-wrapper.properties。不同操作系统的启动脚本也应一起提交,否则 CI 或团队成员可能无法直接构建。
5.9 bin 与 all 分发包
Wrapper 的 distributionUrl 通常指向两种分发包之一:
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zipdistributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-all.zipbin:包含运行 Gradle 所需的二进制内容,体积较小,适合 CI 和一般构建。all:包含二进制、源码和文档,适合需要 IDE 源码导航或离线查阅的开发环境。
两者应保持版本号一致。不要在同一个项目的不同分支中随意切换分发类型,否则可能导致本地缓存、IDE 行为和构建时间差异。
5.10 gradle-wrapper.properties 详解
distributionBase=GRADLE_USER_HOMEdistributionPath=wrapper/distsdistributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zipdistributionSha256Sum=<官方 SHA-256>networkTimeout=10000retries=3retryBackOffMs=1000validateDistributionUrl=truezipStoreBase=GRADLE_USER_HOMEzipStorePath=wrapper/dists主要属性:
distributionUrl:Gradle 分发包地址,是最重要的版本来源。distributionSha256Sum:可选的分发包 SHA-256 校验值,用于防止下载内容被替换或损坏。distributionBase、distributionPath:分发包解压目录的基础位置和相对路径。zipStoreBase、zipStorePath:下载 ZIP 文件的缓存位置。networkTimeout:网络操作超时时间,单位为毫秒。retries:下载失败时的重试次数。retryBackOffMs:重试之间的等待时间。validateDistributionUrl:是否对分发 URL 做额外校验。
属性名称和可用行为应以当前 Wrapper 版本的官方文档为准。不要把凭据直接拼接进 URL,例如不要提交 https://user:password@example.com/...。
5.11 生成 Wrapper 的推荐方式
在已有全局 Gradle 的目录中生成:
gradle wrapper --gradle-version 9.6.1 --distribution-type bin需要源码和文档时:
gradle wrapper --gradle-version 9.6.1 --distribution-type all如果项目已经有 Wrapper,优先使用当前 Wrapper 执行升级任务:
.\gradlew.bat wrapper --gradle-version 9.6.1 --distribution-type bin推荐使用完整的 major.minor.patch 版本,而不是动态版本或只写大版本。这样可以让本地、CI 和未来重建使用相同的分发包。
5.12 Wrapper 升级流程
升级 Wrapper 不等于只修改一行 URL。建议按以下流程执行:
- 阅读目标 Gradle 版本的 Release Notes 和升级说明。
- 核对运行 JVM、Kotlin、Android Gradle Plugin 和第三方插件兼容性。
- 在独立分支运行 Wrapper 升级任务。
- 检查
gradlew、gradlew.bat、Wrapper JAR 和属性文件的差异。 - 第二次运行 Wrapper 任务,使 Wrapper 脚本和 JAR 由目标版本生成。
- 验证
--version、help、tasks、check和build。 - 对比依赖树、测试报告、构件和构建耗时。
- 在干净 CI 环境中重新验证。
示例:
.\gradlew.bat wrapper --gradle-version 9.6.1 --distribution-type bin.\gradlew.bat wrapper.\gradlew.bat --version.\gradlew.bat help --warning-mode all.\gradlew.bat check.\gradlew.bat build升级提交应保持小而可回滚,不要把 Wrapper 升级和业务代码重构混在一个提交中。
5.13 分发包校验
可以在 Wrapper 属性中保存官方校验和:
distributionSha256Sum=<官方 SHA-256>也可以在生成 Wrapper 时传入:
gradle wrapper ` --gradle-version 9.6.1 ` --gradle-distribution-sha256-sum <官方 SHA-256>校验值应从可信的 Gradle 官方发布信息获得,并在代码审查中核对版本、文件名和校验值是否一一对应。
校验解决的是“下载的 Gradle 分发包是否是预期内容”,不能替代:
- Wrapper JAR 的来源审查。
- 构建脚本和插件代码审查。
- 依赖的 Dependency Verification。
- CI 权限和 Secret 管理。
- 生成构件的签名和发布验证。
5.14 Wrapper JAR 验证
gradle-wrapper.jar 会参与构建启动,应该像源代码一样接受审查。团队可以在 CI 中验证 Wrapper JAR 是否来自可信版本和来源。
建议检查:
- Wrapper JAR 是否是项目升级产生的预期变更。
- 文件是否被意外替换或注入额外内容。
distributionUrl是否指向允许的域名。- Wrapper 校验工作流是否在 Pull Request 中执行。
- 依赖和构建脚本是否来自可信分支。
不要从随机博客、Issue 附件或不明项目复制 Wrapper JAR。若历史项目的 Wrapper 文件来源不明,应先在隔离环境中重新生成并审查差异。
5.15 企业内网和私有分发地址
企业可以把 Gradle 分发包放在内部镜像或制品仓库:
distributionUrl=https\://packages.example.com/gradle/gradle-9.6.1-bin.zip此时需要明确:
- 地址是否对开发机和 CI 都可访问。
- TLS 证书是否被所有构建节点信任。
- 分发包是否经过内部校验和审批。
- 镜像是否保留官方文件名和版本关系。
- 是否需要身份认证,以及凭据如何安全注入。
- 离线构建和灾备环境是否有可用副本。
不要通过提交用户名和密码解决私服访问问题。应使用企业代理、机器身份、短期令牌或 CI Secret,并限制读取权限。
5.16 CI 中的 Wrapper 规范
CI 脚本应直接调用项目 Wrapper:
- name: Verify Gradle run: ./gradlew --version
- name: Run checks run: ./gradlew check --no-daemon推荐的 CI 门禁:
- 验证 Wrapper JAR。
- 验证
distributionUrl和分发包校验值。 - 禁止构建脚本自动下载未经批准的第三方 Gradle 分发包。
- 缓存 Gradle User Home 时隔离可信和不可信构建。
- 发布任务只在受保护分支或签名标签中执行。
- 保存 Wrapper 升级、构建失败和分发下载日志。
6. 创建第一个项目
6.1 使用 gradle init
创建 Java 应用:
mkdir gradle-democd gradle-demogradle init --type java-application --dsl kotlin --test-framework junit-jupiter --java-version 21 --project-name gradle-demo也可以执行交互式初始化:
gradle init初始化后使用 Wrapper:
.\gradlew.bat build.\gradlew.bat run6.2 常见项目类型
gradle init 可生成的模板会随版本变化,常见类型包括:
- Basic Build。
- Application。
- Library。
- Gradle Plugin。
- 从 Maven 构建迁移。
查看当前版本支持的选项:
gradle help --task init6.3 推荐初始化选择
对普通 JVM 新项目:
- DSL:Kotlin。
- 测试:JUnit Jupiter。
- Java:由 Toolchain 明确指定。
- 多模块:业务复杂度确实需要时再拆分。
- 包名:使用稳定的反向域名命名空间。
6.4 初始化前的准备
gradle init 会根据当前目录、命令行参数和交互式选择生成构建文件。执行前先确定:
- 项目是 Application、Library、Basic Build 还是 Gradle Plugin。
- 使用 Kotlin DSL 还是 Groovy DSL。
- 使用哪个测试框架。
- Java 项目的 Toolchain 或目标语言版本。
- 包名、项目名、Group 和初始版本。
- 是否生成拆分的多项目结构。
- 当前目录是否为空,以及是否允许覆盖已有文件。
新项目推荐在一个新的目录中初始化:
mkdir gradle-demoSet-Location gradle-demogradle init不要直接在含有重要源码、配置或脚本的目录中执行初始化。先使用版本控制或备份确认变更范围。
6.5 gradle init 的常用参数
查看当前版本支持的完整参数:
gradle help --task init常见参数:
| 参数 | 作用 |
|---|---|
--type | 指定项目模板,如 basic、java-application、java-library |
--dsl | 选择 kotlin 或 groovy DSL |
--test-framework | 选择测试框架,如 junit-jupiter 或 testng |
--java-version | 指定 Java 项目使用的语言版本或 Toolchain 版本 |
--project-name | 指定项目名称 |
--package | 指定生成 Java/Kotlin 源码的包名 |
--insecure-protocol | 明确允许某些不安全仓库协议,生产环境应谨慎 |
--use-defaults | 对可选问题使用默认答案 |
--overwrite | 覆盖已有生成文件,使用前必须审查目录 |
--incubating | 使用当前版本提供的孵化特性或模板选项 |
参数的可用范围会随 Gradle 版本变化。以 gradle help --task init 的输出为准,不要把其他版本的命令行参数直接复制到当前项目。
6.6 常见项目模板
| 模板 | 适用场景 | 常见生成内容 |
|---|---|---|
basic | 只想获得最小 Gradle 构建入口 | Settings、Build Script、Wrapper |
java-application | 可运行的 Java 应用 | application 插件、main 类、测试和运行任务 |
java-library | 可复用 Java Library | java-library 插件、库源码和测试 |
kotlin-application | Kotlin/JVM 应用 | Kotlin 插件、Application 配置和测试 |
kotlin-library | Kotlin/JVM Library | Kotlin Library 配置和测试 |
groovy-application | Groovy 应用 | Groovy 插件、应用入口和测试 |
gradle-plugin | 开发 Gradle Plugin | 插件项目结构、插件测试和发布基础 |
| Maven/POM 转换 | 将已有 Maven 项目转换为 Gradle | 初始 Settings、Build Script 和依赖声明 |
不是所有模板在所有版本中都默认出现。模板列表、参数名称和生成内容应通过 gradle init 的交互提示或 help --task init 确认。
6.7 非交互式初始化
适合脚本、模板仓库和 CI 的初始化命令应显式提供关键参数:
gradle init \ --type java-application \ --dsl kotlin \ --test-framework junit-jupiter \ --java-version 21 \ --project-name hello-gradle \ --package com.example.app \ --use-defaultsWindows PowerShell:
gradle init ` --type java-application ` --dsl kotlin ` --test-framework junit-jupiter ` --java-version 21 ` --project-name hello-gradle ` --package com.example.app ` --use-defaults在团队脚本中,建议把完整命令和生成后的文件一起提交或记录。不要依赖交互式输入顺序,因为不同 Gradle 版本可能增加或调整问题。
6.8 生成文件后的检查
初始化成功后,先不要立即改业务代码,先检查生成结果:
Get-ChildItem -ForceGet-ChildItem -Recurse -File | Select-Object FullName重点查看:
settings.gradle.kts是否包含正确的rootProject.name。build.gradle.kts是否应用预期插件。- 源码包名与目录路径是否一致。
src/main和src/test是否符合项目类型。gradle/wrapper、gradlew和gradlew.bat是否生成。- 是否错误地生成了不需要的示例资源或注释。
gradle.properties是否包含了不应提交的本机配置。
随后运行:
.\gradlew.bat --version.\gradlew.bat projects.\gradlew.bat tasks --all.\gradlew.bat test.\gradlew.bat build6.9 Application、Library 和 Basic Build 的选择
Application
选择 Application 的标志是项目需要一个可运行入口:
plugins { application}
application { mainClass = "com.example.App"}它通常提供 run、installDist、distZip 等任务,适合命令行程序和服务端可执行分发包。
Library
选择 Library 的标志是项目主要被其他模块或项目消费:
plugins { `java-library`}它需要重点设计 api、implementation、发布组件、sources JAR、javadoc JAR 和兼容的 Java 版本。
Basic Build
Basic Build 只适合:
- 学习 Gradle 对象模型。
- 创建聚合构建或构建逻辑项目。
- 逐步添加插件和任务。
- 作为内部模板的空白起点。
不要把 Basic Build 生成的脚本误认为完整 Java 项目。它不会自动提供 Java 编译、测试和运行能力。
6.10 Kotlin DSL、Groovy DSL 与测试框架选择
新项目通常可以选择:
Kotlin DSL + JUnit Jupiter + Java Toolchain选择依据:
- 团队已经大量使用 Kotlin:Kotlin DSL 更容易复用语言经验。
- 团队维护大量 Groovy 构建脚本:继续 Groovy DSL 可能迁移成本更低。
- 新 Java/JVM 项目:JUnit Jupiter 是常见默认选择,但应遵循团队测试规范。
- 需要兼容既有 TestNG 测试:选择 TestNG 或逐步迁移测试框架。
- 需要 Spock:确认 Groovy、Spock 和 Gradle Plugin 版本兼容。
初始化时选择的 DSL 不是永久承诺。可以迁移,但要同时处理插件访问器、Provider API、闭包语义、脚本导入和自定义插件。
6.11 包名、项目名与坐标
初始化时需要区分几个名称:
| 名称 | 作用 | 示例 |
|---|---|---|
| 项目目录 | 文件系统位置 | hello-gradle |
rootProject.name | Gradle 项目身份 | hello-gradle |
| Java/Kotlin 包名 | 源码命名空间 | com.example.app |
group | 发布坐标组织 | com.example |
artifactId | Maven 构件名称 | hello-gradle |
version | 构件版本 | 1.0.0 |
包名应保持稳定,避免把公司内部临时域名、用户名或机器路径写进公共 API。项目目录名可以改变,但发布坐标和源码包名一旦被消费者使用,修改成本会显著增加。
6.12 从 Maven 项目初始化或转换
对于已有 Maven 项目,可以让 Gradle Init Plugin 根据 pom.xml 生成初始 Gradle 文件:
gradle init --type pom也可以在 Maven 项目目录中直接执行交互式初始化,让 Gradle 读取现有 POM。
转换结果只能作为迁移起点,应人工检查:
- Maven Profile 是否被正确表达。
- Plugin Execution 和 Goal 是否映射。
provided、runtime、testScope 是否转换为正确 Configuration。- Parent POM、BOM 和 Dependency Management 是否保留。
- 资源过滤、代码生成、签名和发布配置是否完整。
- Maven 和 Gradle 构建出的类路径与构件是否一致。
迁移完成前,不要删除原始 pom.xml 或停止原 CI 流程。
6.13 初始化后建立 Wrapper
gradle init 通常会生成 Wrapper。确认文件存在:
Test-Path .\gradlew.batTest-Path .\gradlewTest-Path .\gradle\wrapper\gradle-wrapper.jarTest-Path .\gradle\wrapper\gradle-wrapper.properties如果模板或历史项目没有生成 Wrapper,可以补充:
gradle wrapper --gradle-version 9.6.1 --distribution-type bin.\gradlew.bat --version生成 Wrapper 后,后续命令统一改用 gradlew 或 gradlew.bat,不要继续混用全局 Gradle。
6.14 初始化后的 Git 提交边界
第一次提交可以包含:
settings.gradle.kts或settings.gradle。build.gradle.kts或build.gradle。- Wrapper 文件。
src/下的最小源码和测试。gradle.properties中不含敏感信息的公共配置。.gitignore。- README 和构建命令说明。
不应提交:
.gradle/。build/。- 用户目录和机器绝对路径。
- 本地密钥、密码、Token。
- IDE 私有配置,除非团队明确约定。
- 临时下载包和未审查的生成物。
建议初始化后先提交一个“可构建基线”,再逐步添加依赖、插件和业务代码。这样后续问题更容易定位到具体变更。
7. 项目目录与核心文件
典型 Kotlin DSL JVM 项目:
gradle-demo/├── .gradle/ # 项目本地缓存,不提交├── gradle/│ ├── libs.versions.toml # 可选:Version Catalog│ └── wrapper/│ ├── gradle-wrapper.jar│ └── gradle-wrapper.properties├── src/│ ├── main/│ │ ├── java/│ │ └── resources/│ └── test/│ ├── java/│ └── resources/├── build/ # 构建输出,不提交├── build.gradle.kts # 当前项目构建脚本├── settings.gradle.kts # 构建入口与项目结构├── gradle.properties # Gradle 属性├── gradlew└── gradlew.bat7.1 settings.gradle.kts
pluginManagement { repositories { gradlePluginPortal() mavenCentral() }}
dependencyResolutionManagement { repositories { mavenCentral() }}
rootProject.name = "gradle-demo"多项目构建还会声明:
include("app", "core", "service")7.2 build.gradle.kts
plugins { application}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
repositories { mavenCentral()}
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")}
application { mainClass = "com.example.App"}
tasks.test { useJUnitPlatform()}7.3 gradle.properties
适合配置构建行为:
org.gradle.caching=trueorg.gradle.configuration-cache=trueorg.gradle.parallel=trueorg.gradle.warning.mode=all不要把密码、Token、私钥直接提交到项目中的 gradle.properties。
7.4 local.properties
local.properties 常见于 Android 项目,用于保存 Android SDK 等机器本地路径。它不是所有 Gradle 项目的标准必需文件,通常不应提交。
7.5 Gradle User Home
默认目录:
Windows: %USERPROFILE%\.gradleLinux/macOS: ~/.gradle它可能包含下载的 Gradle 分发包、依赖缓存、Daemon 日志、用户级属性和初始化脚本。可通过 GRADLE_USER_HOME 修改位置。
7.6 根项目与子项目目录
单项目构建通常是:
project/├── settings.gradle.kts├── build.gradle.kts├── gradle/├── src/└── gradlew多项目构建通常是:
project/├── settings.gradle.kts├── build.gradle.kts├── gradle/├── app/│ ├── build.gradle.kts│ └── src/├── core/│ ├── build.gradle.kts│ └── src/└── service/ ├── build.gradle.kts └── src/根项目通常负责整个 Build 的入口、聚合任务、插件版本、仓库策略和公共构建逻辑;子项目负责自身的插件、源码、依赖、测试和发布配置。
不要因为根目录有 build.gradle.kts 就把所有模块配置都塞到根脚本。根项目可以是聚合项目,也可以有自己的源码;二者应根据项目职责明确区分。
7.7 settings.gradle(.kts) 的职责
Settings 文件是一次 Gradle Build 的入口,主要负责:
- 设置
rootProject.name。 - 使用
include声明子项目。 - 配置
pluginManagement。 - 配置
dependencyResolutionManagement。 - 创建或导入 Version Catalog。
- 引入 Included Build 和构建逻辑。
- 配置 Build Cache 或全局构建服务。
- 定义项目目录与项目路径的映射。
示例:
pluginManagement { repositories { gradlePluginPortal() mavenCentral() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() }}
rootProject.name = "company-platform"include(":app", ":core", ":service")includeBuild("build-logic")Settings 不适合承载某个模块的业务编译逻辑,也不应直接执行编译、测试或发布动作。它的重点是定义“这个 Build 包含什么以及如何解析”。
7.8 项目路径与目录映射
默认情况下,项目路径和目录结构一致:
include(":app", ":core")对应:
:app → app/:core → core/如果目录名称和项目路径不同,可以显式指定:
include(":frontend")project(":frontend").projectDir = file("web-client")多项目中应尽量保持项目路径、目录名称和模块职责一致。只有迁移旧仓库或兼容既有目录时,才使用复杂映射。
7.9 build.gradle(.kts) 的结构
一个可读的构建脚本通常按以下顺序组织:
plugins { application}
group = "com.example"version = "1.0.0"
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
repositories { mavenCentral()}
dependencies { implementation("com.google.guava:guava:33.5.0-jre") testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")}
application { mainClass = "com.example.App"}
tasks.withType<Test>().configureEach { useJUnitPlatform()}建议的阅读顺序是:插件 → 项目元数据 → Toolchain → 仓库 → 依赖 → 插件 Extension → Task 配置 → 自定义逻辑。
上面的 group 前不应有多余缩进,正确写法是:
group = "com.example"构建脚本应保持短小。如果一个模块的脚本需要大量条件、循环、文件扫描和跨项目修改,应考虑提取 Convention Plugin。
7.10 gradle.properties 的作用和优先级
gradle.properties 可以配置 Gradle 行为和项目属性:
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8org.gradle.caching=trueorg.gradle.configuration-cache=trueorg.gradle.parallel=true常见位置和优先级应以当前 Gradle 文档为准,通常需要区分:
命令行参数 ↓系统属性和项目级 gradle.properties ↓Gradle User Home 中的 gradle.properties ↓环境变量项目级文件适合放团队共同认可的构建行为;用户级文件适合放本机路径、私有仓库凭据和个人默认参数。不要将机器绝对路径和敏感信息放入项目级文件。
区分三类属性:
-Pname=value:项目属性,通常供构建脚本读取。-Dname=value:系统属性,传给 Gradle JVM 或构建环境。org.gradle.*:Gradle 自身的构建行为属性。
例如:
.\gradlew.bat build -PreleaseVersion=1.2.0.\gradlew.bat build -Dorg.gradle.logging.level=info7.11 Init Script 与项目配置的区别
Init Script 位于 Gradle User Home 或通过 -I 参数指定,在 Settings 和项目脚本之前执行。它可以用于企业级仓库镜像、统一监听器或机器级配置,但会影响多个 Build:
.\gradlew.bat build --init-script .\company.init.gradle.kts常见位置:
GRADLE_USER_HOME/init.gradle(.kts)GRADLE_USER_HOME/init.d/*.init.gradle(.kts)GRADLE_HOME/init.d/*.init.gradle(.kts)Init Script 的问题在于它不一定在代码仓库中可见。出现“本机能构建、CI 不能构建”或“同一项目不同用户行为不同”时,应检查用户级 Init Script。
企业可以使用 Init Script 提供镜像或通用规则,但应把它们像生产代码一样版本化、审查和记录。
7.12 Source Set 的目录结构
Java 插件默认提供 main 和 test 两个 Source Set:
src/main/java/ # 生产 Java 源码src/main/resources/ # 生产资源src/test/java/ # 测试 Java 源码src/test/resources/ # 测试资源Source Set 不只是目录,它还关联:
- 源文件集合。
- 资源文件集合。
- 编译类路径。
- 运行时类路径。
- 编译输出目录。
- 依赖 Configuration。
- 对应的编译、资源处理和测试任务。
因此,将文件放入某个目录会影响任务输入和类路径,但不会自动创建所有业务语义。自定义 Source Set 时,需要同时配置依赖、任务和生命周期关系。
7.13 自定义 Source Set
例如创建集成测试 Source Set:
val integrationTest by sourceSets.creating
configurations[integrationTest.implementationConfigurationName] .extendsFrom(configurations.testImplementation.get())
val integrationTestTask = tasks.register<Test>("integrationTest") { description = "运行集成测试" group = "verification" testClassesDirs = integrationTest.output.classesDirs classpath = integrationTest.runtimeClasspath shouldRunAfter(tasks.test)}
tasks.named("check") { dependsOn(integrationTestTask)}还需要创建对应目录:
src/integrationTest/java/src/integrationTest/resources/自定义 Source Set 之前,先确认是否可以使用现有插件提供的测试变体或任务。自行创建过多 Source Set 会增加配置、类路径和 CI 维护成本。
7.14 资源、生成源码与构建输出
应区分三类文件:
手写源码和资源 src/main/
构建过程中生成的源码和资源 build/generated/
编译、测试、打包和报告输出 build/classes/ build/resources/ build/reports/ build/libs/生成源码不应写回 src/main/java,否则会污染版本控制、导致重复编译和 IDE 同步问题。推荐:
val generatedDir = layout.buildDirectory.dir("generated/sources/demo/main")
sourceSets { main { java.srcDir(generatedDir) }}生成任务必须声明输出目录,并让编译任务通过 Provider 或任务依赖消费该目录。
7.15 build/、.gradle/ 与 Gradle User Home
这三个位置容易混淆:
| 目录 | 作用 | 通常提交 |
|---|---|---|
build/ | 当前项目任务生成的构件、类文件和报告 | 否 |
.gradle/ | 当前项目的 Gradle 状态和项目缓存 | 否 |
| Gradle User Home | 用户级分发包、依赖缓存、Daemon 和用户配置 | 否 |
gradle/ | 项目维护的 Wrapper、Version Catalog 等文件 | 是 |
推荐 .gitignore:
.gradle/build/!gradle/wrapper/gradle-wrapper.jar!gradle/wrapper/gradle-wrapper.properties不要使用 git add . 之后再想办法排除构建缓存。先建立正确的 .gitignore,再提交源码和构建契约。
7.16 Android 项目目录差异
Android 项目仍使用 Settings、Build Script、Gradle User Home 和 Wrapper,但 Source Set 和输出目录由 Android Gradle Plugin 扩展:
android-project/├── settings.gradle.kts├── build.gradle.kts├── gradle.properties├── gradle/├── app/│ ├── build.gradle.kts│ └── src/│ ├── main/│ ├── debug/│ ├── release/│ ├── test/│ └── androidTest/├── local.properties└── gradlew.batlocal.properties 通常记录本机 Android SDK 路径,不应提交。src/debug、src/release、Flavor 和组合 Variant 的优先级由 AGP 决定,不能只按普通 Java Source Set 的直觉理解。
7.17 目录和文件命名规范
推荐:
- 项目目录使用小写、短横线或团队统一命名。
- 子项目目录与项目路径保持一致。
- Kotlin DSL 文件统一使用
.gradle.kts。 - 构建逻辑放在
build-logic/或约定目录,不与业务源码混放。 - 生成输出统一放在
build/下。 - 报告按工具和任务区分目录。
- 公共版本目录放在
gradle/libs.versions.toml。
避免:
- 使用
src目录同时存放手写源码和生成源码。 - 在多个位置复制同一份仓库或 JDK 配置。
- 用
local.properties保存团队共享设置。 - 用随机目录名或机器用户名作为项目身份。
- 将下载的外部 JAR 放进
libs/后长期不治理。
7.18 观察项目结构的命令
.\gradlew.bat projects.\gradlew.bat tasks --all.\gradlew.bat properties.\gradlew.bat buildEnvironment.\gradlew.bat dependenciesPowerShell 查看文件树:
Get-ChildItem -ForceGet-ChildItem -Recurse -File | Select-Object FullNameLinux/macOS:
find . -maxdepth 3 -type f | sort./gradlew projects./gradlew tasks --all./gradlew properties这些命令可以帮助确认:项目是否被 Settings 包含、插件是否生效、属性是否加载、任务是否生成以及依赖从哪里来。
8. Kotlin DSL 与 Groovy DSL
8.1 文件名
| 用途 | Kotlin DSL | Groovy DSL |
|---|---|---|
| Settings | settings.gradle.kts | settings.gradle |
| Build Script | build.gradle.kts | build.gradle |
| 初始化脚本 | init.gradle.kts | init.gradle |
8.2 Kotlin DSL 示例
plugins { java}
dependencies { implementation("com.google.guava:guava:33.5.0-jre")}
tasks.register("hello") { doLast { println("Hello Gradle") }}8.3 Groovy DSL 示例
plugins { id 'java'}
dependencies { implementation 'com.google.guava:guava:33.5.0-jre'}
tasks.register('hello') { doLast { println 'Hello Gradle' }}8.4 如何选择
新项目通常优先 Kotlin DSL,因为它的静态类型检查、IDE 补全和重构体验更好。维护现有 Groovy DSL 项目时不必为了统一而立即迁移;迁移会涉及类型、闭包、扩展访问器和插件 API 差异,应分阶段完成并持续验证。
8.5 Kotlin DSL 常见注意点
- 字符串通常使用双引号。
- 插件短名可以写成
java、application。 - 带连字符的核心插件使用反引号,例如
`java-library`。 - 属性常用赋值语法或 Provider API,而不是 Groovy 的动态调用。
- 不要把普通 Kotlin 应用代码习惯机械套进构建脚本。
8.6 两种 DSL 共享同一个 Gradle 模型
Kotlin DSL 和 Groovy DSL 只是表达 Gradle 模型的两种语言入口。无论使用哪一种 DSL,最终配置的仍然是同一套对象:
Kotlin DSL / Groovy DSL ↓Settings、Project、Plugin、Extension、Configuration、Task ↓相同的任务图、依赖解析和构建输出因此,DSL 迁移不应该改变项目的构建契约。迁移前后应比较:
- 应用的插件和插件版本。
- 仓库和依赖声明。
- Task 名称、输入输出和依赖关系。
- 编译参数、测试框架和 Toolchain。
- 发布构件、POM、Module Metadata 和报告。
如果迁移后只是“脚本能编译”,但任务图、类路径或发布物发生了非预期变化,迁移仍未完成。
8.7 Kotlin DSL 的类型安全访问器
Kotlin DSL 会根据已应用的插件和模型生成类型安全访问器:
plugins { java application}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
application { mainClass = "com.example.App"}这里的 java {}、application {}、implementation(...) 和 tasks.test {} 都依赖插件提供的模型和访问器。
访问器的生成时机带来几个限制:
- 插件应优先在
plugins {}块中应用。 - 访问器只对当前脚本已经可见的模型生成。
- 在脚本插件、Init Script 或非常动态的配置中,可能无法使用同样的类型安全访问器。
- 后续动态创建的 Extension 或 Configuration 不一定有静态访问器。
- 当访问器不可用时,可以使用字符串名称和类型安全 API。
例如:
plugins { java}
val customClasspath = configurations.create("customClasspath")
customClasspath.dependencies.add( dependencies.create("com.example:tool:1.0.0"))不要把“有类型安全访问器”理解为“所有 Gradle API 都能像普通 Kotlin 库一样静态推断”。访问器依赖当前插件和脚本模型。
8.8 Groovy DSL 的闭包与委托
Groovy DSL 大量使用 Closure。一个配置块中的属性和方法,往往通过 Closure delegate 指向 Gradle 的 Extension、Task 或其他对象:
android { defaultConfig { minSdk 26 }}这种写法简洁,但初学者容易误判变量来源。遇到同名属性、嵌套闭包或插件扩展时,应确认当前 Closure 的委托对象。
可以显式调用对象方法减少歧义:
tasks.named('test', Test) { useJUnitPlatform()}Groovy 的动态特性适合快速编写和兼容历史脚本,但错误可能在运行配置阶段才暴露,例如拼写错误被当成动态属性、方法重载选择不符合预期或闭包嵌套导致对象引用错误。
8.9 常见语法对照
| 场景 | Kotlin DSL | Groovy DSL |
|---|---|---|
| 应用 Java 插件 | java | id 'java' |
| 应用外部插件 | id("x") version "1.0" | id 'x' version '1.0' |
| 字符串 | "value" | 'value' 或 "value" |
| 依赖 | implementation("g:a:v") | implementation 'g:a:v' |
| 项目依赖 | implementation(project(":core")) | implementation project(':core') |
| 文件路径 | file("config") | file('config') |
| 任务注册 | tasks.register("hello") | tasks.register('hello') |
| 任务类型 | tasks.register<Copy>("copy") | tasks.register('copy', Copy) |
| Provider | providers.gradleProperty("x") | providers.gradleProperty('x') |
| Map | mapOf("key" to "value") | [key: 'value'] |
| 列表 | listOf("a", "b") | ['a', 'b'] |
| 空值 | null | null |
不要只做字符替换。Kotlin DSL 需要明确类型、泛型、Property 和 Provider;Groovy DSL 需要注意闭包委托、动态属性和方法调用语法。
8.10 plugins 块对比
Kotlin DSL:
plugins { java application id("org.jetbrains.kotlin.jvm") version "2.3.20"}Groovy DSL:
plugins { id 'java' id 'application' id 'org.jetbrains.kotlin.jvm' version '2.3.20'}根项目统一管理版本:
plugins { id("org.jetbrains.kotlin.jvm") version "2.3.20" apply false}plugins { id 'org.jetbrains.kotlin.jvm' version '2.3.20' apply false}现代项目优先使用 plugins {},因为它支持更清晰的插件解析、版本管理和静态模型。历史项目中的 buildscript {} 和 apply plugin: 可以继续维护,但不应在新代码中无计划扩散。
8.11 依赖声明对比
Kotlin DSL:
dependencies { implementation(libs.guava) testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") runtimeOnly(project(":runtime-support"))}Groovy DSL:
dependencies { implementation libs.guava testImplementation 'org.junit.jupiter:junit-jupiter:6.0.3' runtimeOnly project(':runtime-support')}复杂依赖需要显式配置排除和约束:
dependencies { implementation("org.example:library:1.0.0") { exclude(group = "org.example", module = "legacy-helper") }}dependencies { implementation('org.example:library:1.0.0') { exclude group: 'org.example', module: 'legacy-helper' }}依赖 DSL 只是声明入口,最终结果仍由 Configuration、仓库、传递依赖、变体和解析规则决定。
8.12 Task 注册与懒配置
Kotlin DSL 推荐:
val printVersion by tasks.registering { group = "help" description = "打印项目版本"
doLast { println(project.version) }}
tasks.named("check") { dependsOn(printVersion)}Groovy DSL:
tasks.register('printVersion') { group = 'help' description = '打印项目版本'
doLast { println project.version }}
tasks.named('check') { dependsOn 'printVersion'}两种 DSL 都应优先使用 register、named 和 configureEach,避免在配置阶段创建和配置所有任务:
tasks.withType<Test>().configureEach { useJUnitPlatform()}8.13 Property、Provider 和空值
Kotlin DSL 通常更早暴露类型和空值问题:
val releaseVersion = providers.gradleProperty("releaseVersion") .orElse("0.0.0-SNAPSHOT")
version = releaseVersion.get()Groovy DSL 可以写成:
def releaseVersion = providers.gradleProperty('releaseVersion') .orElse('0.0.0-SNAPSHOT')
version = releaseVersion.get()推荐两种 DSL 都使用 Provider API,而不是直接读取环境变量并立即计算:
val repositoryUser = providers.environmentVariable("REPOSITORY_USER")def repositoryUser = providers.environmentVariable('REPOSITORY_USER')只有在确实需要值时才调用 .get()。过早求值会增加配置阶段工作,也可能降低 Configuration Cache 的兼容性。
8.14 Extension 与容器配置
插件扩展通常有嵌套配置和命名对象容器。
Kotlin DSL:
application { mainClass = "com.example.App"}
sourceSets { named("main") { resources.srcDir("src/main/generated-resources") }}Groovy DSL:
application { mainClass = 'com.example.App'}
sourceSets { main { resources.srcDir 'src/main/generated-resources' }}当对象名称来自变量或运行时配置时,Kotlin DSL 可以使用 named、getting、register;Groovy DSL 可以使用字符串和闭包。对于复杂插件,优先使用插件公开的类型化 Extension,而不是修改内部任务或容器。
8.15 Settings Script、Build Script 与 Script Plugin
三类脚本的作用域不同:
| 脚本 | 主要对象 | 典型用途 |
|---|---|---|
settings.gradle.kts | Settings | 构建边界、项目、插件和仓库 |
build.gradle.kts | Project | 插件、依赖、任务和模块配置 |
*.gradle.kts 脚本插件 | 被应用脚本的对象 | 小型共享规则或迁移兼容 |
init.gradle.kts | Gradle/全局构建 | 用户级或企业级初始化 |
Kotlin DSL 和 Groovy DSL 可以分别用于不同脚本,但不应仅为了“混合语法”而拆分配置。大型共享逻辑更适合放入预编译脚本插件或二进制 Convention Plugin。
8.16 两种 DSL 混用
一个项目可以暂时同时存在:
settings.gradle.ktsbuild.gradle.ktslegacy.gradlebuild-logic/src/main/groovy/...也可以在 Kotlin DSL 中应用历史脚本:
apply(from = "legacy.gradle")混用时注意:
- 脚本之间共享的变量和 Extra Property 可能缺少类型约束。
- Groovy 闭包和 Kotlin Lambda 的委托语义不同。
- Kotlin DSL 的类型安全访问器不会自动出现在 Groovy 脚本中。
- IDE 代码补全和错误定位可能跨脚本变差。
- 应逐步把公共逻辑迁移到类型明确的 Convention Plugin。
8.17 Groovy 迁移到 Kotlin DSL
推荐按模块或脚本逐步迁移:
- 记录迁移前的 Gradle、JDK、插件和构建输出。
- 先迁移 Settings,再迁移根项目或一个简单子项目。
- 将
apply plugin:改为plugins {},确认插件版本和应用顺序。 - 将字符串依赖、任务和 Extension 配置改为 Kotlin 语法。
- 把动态属性转换为
Property<T>、Provider<T>和显式类型。 - 把复杂闭包改写为类型安全的
named、register和configureEach。 - 运行
help、tasks、test、check、build和发布验证。 - 删除无用的脚本插件和 Extra Property。
常见迁移替换:
| Groovy | Kotlin DSL |
|---|---|
id 'java' | java |
implementation 'g:a:v' | implementation("g:a:v") |
tasks.register('x') | tasks.register("x") |
tasks.named('test', Test) | tasks.named<Test>("test") |
sourceSets.main | sourceSets.named("main") |
ext.foo = 'bar' | 明确的 Extension、Property 或 Provider |
project(':core') | project(":core") |
file('src') | file("src") |
迁移时不要把所有动态值都简单替换成 val。先判断它是普通值、Provider、Property、Task Provider 还是文件集合。
8.18 Kotlin DSL 的编译和性能影响
Kotlin DSL 提供类型检查,但构建脚本也需要编译和访问器生成。以下做法有助于降低影响:
- 使用
plugins {}让插件模型尽早可见。 - 通过 Convention Plugin 复用复杂逻辑。
- 避免在每个模块复制大型脚本。
- 使用懒任务 API,减少配置阶段对象创建。
- 不在脚本顶层执行网络和大规模文件操作。
- 将稳定构建逻辑提取到
build-logic,而不是不断增加脚本长度。 - 通过 Configuration Cache 评估实际收益,而不是猜测。
Groovy DSL 也可以写出高性能构建;性能差异通常来自配置方式、插件实现和任务输入输出,而不是 DSL 名称本身。
8.19 IDE 与错误排查
Kotlin DSL 常见错误:
Unresolved reference:插件未应用、访问器未生成或类型导入缺失。Type mismatch:Property、Provider、普通值或文件类型混用。Cannot access ...:插件 API 或 Gradle 版本不兼容。The value for this property is final:属性已在不允许的阶段被锁定。
Groovy DSL 常见错误:
- 方法或属性拼写错误直到配置阶段才暴露。
- 闭包委托对象错误。
- 字符串、Map、List 和方法调用语法混淆。
ext或动态属性来源不清。
通用诊断:
.\gradlew.bat help --stacktrace.\gradlew.bat tasks --all --info.\gradlew.bat build --warning-mode all如果 IDE 报错而命令行正常,先刷新 Gradle 模型并确认 IDE 使用的 Wrapper、JDK 和 Gradle JVM。
8.20 DSL 风格规范
建议团队统一:
- 新项目使用哪种 DSL。
- 插件块、元数据、仓库、依赖和任务的排列顺序。
- 字符串、命名、缩进和换行规则。
- 任务注册和 Provider API 的写法。
- 何时允许脚本插件,何时必须使用 Convention Plugin。
- 是否允许
allprojects、subprojects、ext和动态属性。 - 迁移时需要运行的验证命令。
可以通过格式化工具、代码审查和 CI 检查构建脚本,但不要只依赖格式化工具解决对象模型和生命周期问题。
9. 构建生命周期
Gradle 构建主要包含三个阶段:初始化、配置、执行。
9.1 Initialization 初始化
主要工作:
- 读取初始化脚本。
- 定位并执行 Settings 文件。
- 确定根项目和子项目。
- 创建 Settings 和 Project 模型。
9.2 Configuration 配置
主要工作:
- 执行参与构建的项目脚本。
- 应用插件。
- 注册和配置任务。
- 构建任务模型。
配置阶段应该尽量快速、确定、无副作用。不要在配置阶段执行昂贵文件扫描、网络请求、外部进程或业务计算。
9.3 Execution 执行
主要工作:
- 根据用户请求选择任务。
- 构建任务依赖图。
- 判断任务是否需要执行。
- 按依赖和顺序规则执行任务动作。
9.4 观察生命周期
println("配置 build.gradle.kts")
tasks.register("hello") { println("配置 hello 任务")
doLast { println("执行 hello 任务") }}执行:
.\gradlew.bat hello位于任务配置闭包主体中的 println 发生在任务被配置时;doLast 中的代码属于任务动作,在执行阶段运行。
9.5 生命周期常见误区
- 构建脚本顶层代码不是“执行任务时才运行”。
- 注册任务不等于执行任务。
- 配置阶段成功不代表依赖解析和任务执行一定成功。
dependsOn表示依赖关系,不等于普通函数调用。- 执行顺序应主要由任务输入输出和依赖关系决定,而不是大量手工顺序规则。
9.6 一次构建的更细时间线
在不考虑特殊插件和配置缓存细节时,可以用下面的时间线理解一次构建:
启动 Gradle ↓读取 Init Script ↓读取 Settings Script ↓确定根项目、子项目和 Included Build ↓创建 Settings / Project 模型 ↓执行各项目 Build Script ↓应用 Plugin、创建 Extension、Configuration 和 Task ↓根据用户请求构造 Task Graph ↓执行任务输入检查和依赖解析 ↓执行 Task Action ↓生成输出、报告和构件更准确地说,Gradle 先决定 Build 的结构,再配置参与构建的 Project,最后根据任务图执行用户请求的任务。不是所有项目中的每个任务都会执行,也不是每个依赖都会在脚本读取时立即下载。
9.7 初始化阶段的重点
初始化阶段主要回答:
这次 Build 有哪些项目?哪些 Included Build 参与?使用哪些插件仓库和依赖仓库策略?典型顺序:
- 加载 Init Script。
- 定位 Settings 文件。
- 创建
Settings对象。 - 执行
pluginManagement和 Settings 插件配置。 - 执行
include、includeBuild和项目目录映射。 - 创建参与构建的 Project 实例。
初始化阶段出错时,通常还没有进入具体模块的 Build Script。例如:
- Settings 文件语法错误。
- 子项目路径拼写错误。
- 插件仓库无法访问。
- Included Build 不存在。
- 项目目录映射指向不存在的目录。
排查命令:
.\gradlew.bat projects --stacktrace.\gradlew.bat help --info9.8 配置阶段的重点
配置阶段主要回答:
每个 Project 如何构建?有哪些 Plugin、Extension、Configuration 和 Task?用户请求的 Task Graph 是什么?Gradle 会评估参与构建的项目脚本,应用插件,注册任务,配置依赖和扩展,并根据用户请求计算任务图。
以下代码在配置阶段执行:
println("build script configured")
tasks.register("demo") { println("task configured")
doLast { println("task executed") }}执行 demo 时通常看到:
build script configuredtask configuredtask executed如果执行一个不需要 demo 的任务,build script configured 仍可能出现,因为项目脚本需要先被评估;而 task executed 不会出现。
9.9 执行阶段的重点
执行阶段主要回答:
任务图中哪些 Task 真正需要执行?哪些任务可以跳过或从缓存恢复?任务动作按什么顺序运行?Gradle 会根据:
- Task 依赖关系。
- Task 输入和输出。
onlyIf条件。- up-to-date 状态。
- Build Cache 命中情况。
- 失败策略和排除参数。
- 并行执行约束。
决定每个 Task 的执行行为。
任务动作应尽量只处理声明的输入,并生成声明的输出:
abstract class GenerateFile : DefaultTask() { @get:Input abstract val content: Property<String>
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val file = outputFile.get().asFile file.parentFile.mkdirs() file.writeText(content.get()) }}9.10 Task Graph 是有向无环图
Gradle 使用任务依赖关系形成 DAG(Directed Acyclic Graph,有向无环图):
prepare → compile → classes → jar → assemble └────→ test ───→ check ↓ build特点:
- 一个任务可以被多个任务依赖。
- 一个任务只会在任务图中出现一次。
- 没有依赖关系的任务可以并行。
- 任务依赖不能形成循环。
- 用户请求的任务决定任务图的子集。
例如:
val generate by tasks.registering { doLast { println("generate") }}
tasks.named("compileJava") { dependsOn(generate)}执行 test、classes 或 build 时,只有在这些任务的依赖路径包含 compileJava 的情况下,generate 才会进入任务图。
9.11 生命周期任务与动作任务
生命周期任务通常用于聚合其他任务,不一定直接处理文件:
build├── assemble└── check ├── test └── quality checks常见生命周期任务:
assemble:聚合构建产物生成。check:聚合测试和质量检查。build:通常聚合assemble和check。clean:清理项目构建输出。
动作任务负责具体工作,例如 compileJava、processResources、test 和 jar。不要假设生命周期任务本身有一个“隐藏的编译动作”;应通过 tasks --all 和任务依赖查看真实关系。
9.12 dependsOn 与输入输出的生命周期意义
如果 B 读取 A 的输出,推荐同时表达数据关系和执行关系:
val generated = tasks.register<GenerateFile>("generated") { content.set("hello") outputFile.set(layout.buildDirectory.file("generated/message.txt"))}
tasks.named("processResources") { from(generated.map { it.outputFile })}当任务使用 Provider 连接另一个任务的输出时,Gradle 可以推断依赖关系。相比只写字符串任务名,这种方式更利于:
- 增量构建。
- Build Cache。
- 并行执行。
- Configuration Cache。
- 输出目录冲突检查。
只写 mustRunAfter 不能表达数据依赖,也不能保证目标任务会进入任务图。
9.13 配置阶段的反模式
以下代码会让配置阶段变慢、不可复现或难以使用 Configuration Cache:
val remoteVersion = URL("https://example.com/version").readText()val allFiles = fileTree(".").filesval output = "build/${System.currentTimeMillis()}"问题包括:
- 网络请求使构建依赖外部服务。
- 全仓库扫描增加启动成本。
- 当前时间导致每次配置输入变化。
- 配置阶段读取的值不一定被 Gradle 正确声明。
- 失败位置远离实际任务动作。
改进方向:
- 将网络和外部工具调用放到明确的 Task。
- 用
fileTree("src")或精确目录缩小输入范围。 - 使用 Provider 和 Property 延迟读取值。
- 将时间、环境变量和文件内容声明为输入。
- 使用 Build Service 管理共享资源。
9.14 生命周期钩子
Gradle 提供 settingsEvaluated、projectsLoaded、beforeProject、afterProject 和 projectsEvaluated 等生命周期钩子。
它们适合诊断、企业级初始化或必须在特定阶段执行的基础设施逻辑:
gradle.projectsEvaluated { logger.lifecycle("all projects have been evaluated")}但不建议大量依赖 afterEvaluate 和全局生命周期监听器:
- 执行顺序隐式。
- 可能覆盖项目自己的配置。
- 难以组合多个插件。
- 可能影响 Configuration Cache。
- 容易造成根项目和子项目耦合。
优先使用:
- 插件公开的 Extension。
plugins.withId {}。tasks.withType<T>().configureEach {}。- Provider 和 Property 连接值。
- Convention Plugin。
9.15 配置缓存对生命周期的影响
Configuration Cache 会缓存配置阶段结果和任务图。命中后,Gradle 可以跳过部分甚至全部项目配置,直接加载任务图并进入执行阶段:
.\gradlew.bat build --configuration-cache.\gradlew.bat build --configuration-cache第一次运行通常需要建立缓存;第二次运行相同任务请求时才可能命中。
配置缓存关注的不只是 Build Script,还可能受到以下输入影响:
- Init Script。
- Settings 和 Build Script。
gradle.properties。- Version Catalog、锁文件和验证文件。
- 配置阶段读取的文件内容。
- 配置阶段使用的系统属性和环境变量。
- Gradle Daemon JVM 和 Gradle User Home。
启用后,任务动作不能依赖不可序列化的 Project、Gradle 或 Configuration 对象。应把值转换成 Property、Provider、文件输入或共享 Build Service。
9.16 依赖解析发生在哪个阶段
依赖声明通常发生在配置阶段:
dependencies { implementation("com.example:library:1.0.0")}真正的依赖图和构件下载可以延迟到任务执行需要时。但以下代码可能在配置阶段立即触发解析:
val files = configurations.runtimeClasspath.files这会带来:
- 配置阶段网络请求。
- 配置缓存输入增加。
- 依赖解析错误更早出现。
- 构建启动时间变长。
如果任务需要依赖文件,应将 Configuration 或 FileCollection 作为任务输入,让 Gradle 在合适的阶段解析:
tasks.register("printClasspath") { inputs.files(configurations.runtimeClasspath) doLast { configurations.runtimeClasspath.get().forEach(::println) }}9.17 生命周期诊断命令
.\gradlew.bat help.\gradlew.bat tasks --all.\gradlew.bat build --dry-run.\gradlew.bat build --info.\gradlew.bat build --profile.\gradlew.bat build --scan.\gradlew.bat build --configuration-cache诊断顺序:
help:确认 Settings、插件和配置阶段可以成功。tasks --all:确认任务是否存在。--dry-run:查看任务图计划。--info:查看跳过、依赖和执行原因。--profile或--scan:定位慢的阶段和任务。--configuration-cache:识别配置阶段副作用和不兼容逻辑。
9.18 生命周期实践记录
排查生命周期问题时,记录以下信息:
执行命令:Wrapper/Gradle 版本:JDK 和 Gradle JVM:参与项目:用户请求的任务:实际任务图:配置阶段日志:执行阶段日志:任务状态:缓存状态:失败位置:可以给任务和脚本增加临时日志:
logger.lifecycle("configuring ${project.path}")
tasks.register("lifecycleProbe") { doFirst { logger.lifecycle("execution started") } doLast { logger.lifecycle("execution finished") }}诊断完成后应删除或降级临时日志,避免污染正常构建输出。
10. Task 任务系统
10.1 查看任务
.\gradlew.bat tasks.\gradlew.bat tasks --all.\gradlew.bat help --task build.\gradlew.bat build --dry-run10.2 注册任务
推荐使用配置规避 API:
val hello by tasks.registering { group = "demo" description = "输出问候语"
doLast { println("Hello Gradle") }}尽量避免使用会立即创建和配置任务的旧式写法。
10.3 任务依赖
val prepare by tasks.registering { doLast { println("prepare") }}
val packageApp by tasks.registering { dependsOn(prepare) doLast { println("package") }}10.4 finalizedBy 与顺序规则
tasks.named("test") { finalizedBy("testReportSummary")}
taskB { mustRunAfter(taskA)}finalizedBy:主任务结束后执行终结任务。mustRunAfter:两者都进入任务图时强制顺序。shouldRunAfter:软顺序约束。- 顺序规则不会自动把另一个任务加入任务图。
如果 B 必须依赖 A 的输出,应使用 dependsOn 或直接连接输入输出。
10.5 条件执行
tasks.register("releaseOnly") { onlyIf("仅在 release 属性为 true 时执行") { providers.gradleProperty("release").orNull == "true" } doLast { println("release") }}.\gradlew.bat releaseOnly -Prelease=true10.6 跳过和重新执行
.\gradlew.bat build -x test.\gradlew.bat build --rerun-tasks.\gradlew.bat build --continue-x test会降低验证质量,不宜成为常态。--rerun-tasks忽略 up-to-date 判断。--continue在失败后继续执行其他独立任务,便于一次收集多个问题。
10.7 Task 的状态与执行结果
Gradle 执行任务时会根据输入、输出、条件和缓存产生不同结果:
| 状态 | 含义 |
|---|---|
EXECUTED | Task Action 实际执行 |
UP-TO-DATE | 输入没有变化,已有输出仍然有效 |
FROM-CACHE | 从 Build Cache 恢复输出 |
NO-SOURCE | 没有可处理的源文件 |
SKIPPED | 条件不满足、任务被排除或任务明确跳过 |
FAILED | Task Action 或依赖任务失败 |
观察状态:
.\gradlew.bat build --info.\gradlew.bat build --profile不要把 UP-TO-DATE 当成“什么都没发生”。Gradle 仍然检查了任务输入、输出和依赖状态,只是判断不需要重复执行。
10.8 常见内置 Task 类型
Gradle 核心插件和语言插件会提供许多可配置 Task 类型:
| Task 类型 | 典型用途 |
|---|---|
JavaCompile | 编译 Java 源码 |
Test | 运行 JVM 测试 |
Jar | 生成 JAR |
Copy | 复制文件和目录 |
Sync | 同步目录并删除过期输出 |
Delete | 删除文件或目录 |
Exec | 执行外部进程 |
JavaExec | 使用指定 JVM 执行 Java 主类 |
Zip / Tar | 创建压缩包 |
WriteProperties | 生成 .properties 文件 |
优先配置插件提供的 Task,而不是重新实现同样功能:
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8"}
tasks.withType<Test>().configureEach { useJUnitPlatform()}10.9 Task 的注册、配置与执行
Task 的生命周期可以拆成三个动作:
注册 Task:创建任务 Provider配置 Task:设置属性、输入输出、依赖和动作执行 Task:运行 Task Action推荐写法:
val packageInfo by tasks.registering { group = "build setup" description = "生成构建信息"
doLast { println("version=${project.version}") }}配置已有任务:
tasks.named<Jar>("jar") { archiveBaseName.set("demo")}按类型配置所有相关任务:
tasks.withType<Copy>().configureEach { include("**/*.properties")}不推荐:
tasks.getByName("jar") { // 立即查找并配置,可能触发不必要的 eager configuration}10.10 Task Provider 与懒配置
TaskProvider<T> 表示一个尚未完全实现的 Task 引用:
val generate by tasks.registering(GenerateFile::class) { content.set("hello") outputFile.set(layout.buildDirectory.file("generated/message.txt"))}
tasks.named("processResources") { dependsOn(generate)}使用 Provider 的好处:
- 延迟创建任务。
- 允许 Gradle 跳过不需要的 Task。
- 更容易表达任务依赖和文件依赖。
- 改善 Configuration Cache 和并行配置兼容性。
- 避免插件应用顺序导致的任务查找失败。
10.11 Task Action 的执行顺序
tasks.register("lifecycle") { doFirst { println("first action") }
doLast { println("last action") }}doFirst 会把动作插入当前动作列表的前面,doLast 会追加到后面。多个 doFirst 的执行顺序容易造成阅读障碍,因此不应把 Task 当作任意代码片段容器。
更适合的做法是定义一个职责清晰的 Task 类型:
abstract class GenerateReport : DefaultTask() { @get:Input abstract val title: Property<String>
@get:OutputFile abstract val reportFile: RegularFileProperty
@TaskAction fun generate() { val file = reportFile.get().asFile file.parentFile.mkdirs() file.writeText("# ${title.get()}\n") }}10.12 Task 输入和输出
Task 是否能够增量执行和缓存,取决于是否完整声明输入输出:
abstract class TransformText : DefaultTask() { @get:InputFile abstract val sourceFile: RegularFileProperty
@get:Input abstract val prefix: Property<String>
@get:OutputFile abstract val targetFile: RegularFileProperty
@TaskAction fun transform() { val source = sourceFile.get().asFile.readText() val target = targetFile.get().asFile target.parentFile.mkdirs() target.writeText(prefix.get() + source) }}常用声明:
@Input:字符串、数字、布尔值等简单输入。@Optional:允许输入没有值。@InputFile:单个文件输入。@InputFiles:多个文件输入。@InputDirectory:目录输入。@OutputFile:单个文件输出。@OutputDirectory:目录输出。@Nested:嵌套的输入对象。@Internal:明确不参与状态计算的属性。
不要把会影响输出的属性标记为 @Internal,也不要把每次都会变化但不影响结果的临时文件标记为输入。
10.13 文件输入的规范化
文件输入不仅有路径,还涉及路径敏感性、内容敏感性和目录结构。Gradle 提供文件集合和路径规范化能力:
abstract class ProcessResources : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val inputs: ConfigurableFileCollection
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
@TaskAction fun process() { inputs.files.forEach { input -> val output = outputDirectory.file(input.name).get().asFile output.parentFile.mkdirs() output.writeText(input.readText()) } }}选择路径敏感性时要根据任务实际需求:
ABSOLUTE:绝对路径会影响结果。RELATIVE:相对于输入根目录的路径会影响结果。NAME_ONLY:只关心文件名。NONE:只关心内容,不关心路径。
错误的规范化设置可能导致缓存误命中或不必要的重新执行。
10.14 可缓存 Task
自定义 Task 如果输出完全由声明的输入决定,可以考虑标记为可缓存:
@CacheableTaskabstract class GenerateManifest : DefaultTask() { @get:Input abstract val entries: ListProperty<String>
@get:OutputFile abstract val manifestFile: RegularFileProperty
@TaskAction fun writeManifest() { val file = manifestFile.get().asFile file.parentFile.mkdirs() file.writeText(entries.get().sorted().joinToString("\n")) }}可缓存 Task 必须满足:
- 所有影响输出的值都是输入。
- 输出不依赖当前时间、随机数、用户目录或隐藏网络状态。
- 输出内容不包含机器特定或敏感数据。
- 输入和输出可稳定快照。
- 不通过未声明的全局状态改变结果。
如果任务不安全缓存,可以显式禁用缓存并说明原因,而不是提供可能错误的缓存结果。
10.15 Task 依赖和数据依赖
执行依赖:
tasks.named("packageApp") { dependsOn("compileJava")}数据依赖:
val generated = tasks.register<GenerateFile>("generated") { content.set("hello") outputFile.set(layout.buildDirectory.file("generated/message.txt"))}
tasks.named<Copy>("processResources") { from(generated.map { it.outputFile })}数据依赖通常比字符串形式的 dependsOn 更完整,因为它同时告诉 Gradle:
- 哪个任务必须先执行。
- 哪个文件是输入。
- 输出发生变化时如何判断任务状态。
- 哪些任务可以缓存或并行。
10.16 Task 命令行选项
自定义 Task 可以暴露命令行参数:
abstract class PrintMessage : DefaultTask() { @get:Input abstract val message: Property<String>
@Option(option = "message", description = "要输出的消息") fun setMessage(value: String) { message.set(value) }
@TaskAction fun print() { logger.lifecycle(message.get()) }}执行:
.\gradlew.bat printMessage --message=hello命令行选项应有清晰的类型、默认值、描述和输入声明。不要用全局变量传递任务参数,也不要把敏感信息作为普通命令行参数,因为命令可能出现在进程列表和 CI 日志中。
10.17 Task 条件与跳过
tasks.register("publishSnapshot") { onlyIf("仅在 snapshot=true 时执行") { providers.gradleProperty("snapshot").orNull == "true" }}命令行排除:
.\gradlew.bat build --exclude-task test重新执行:
.\gradlew.bat build --rerun-tasksonlyIf是任务自己的条件。--exclude-task会从任务图中排除指定任务及其执行路径,可能使构建失去验证意义。--rerun-tasks忽略 up-to-date 判断,适合诊断而不是日常构建。
10.18 聚合任务与自定义生命周期
可以创建只负责聚合其他任务的生命周期 Task:
tasks.register("qualityGate") { group = "verification" description = "运行项目质量门禁" dependsOn("test", "checkstyleMain", "jacocoTestReport")}聚合任务应该:
- 主要使用
dependsOn。 - 不直接写入业务输出。
- 不在
doLast中重复调用其他 Gradle Task。 - 有清晰的
group和description。 - 可以在本地、CI 和发布流程中复用。
不要在一个 Task 的 doLast 中调用 tasks.named("other").get().actions 或手工执行另一个 Task,这会绕过 Gradle 的任务图和状态管理。
10.19 外部进程和文件操作
外部命令应在 Task Action 中执行:
tasks.register<Exec>("generateDocs") { commandLine("java", "-version")}如果需要根据输入动态配置命令,使用 Provider 和明确的输入:
abstract class RunTool : DefaultTask() { @get:InputFile abstract val inputFile: RegularFileProperty
@TaskAction fun runTool() { project.exec { commandLine("tool", inputFile.get().asFile.absolutePath) } }}生产项目还要考虑:
- 外部程序是否跨平台。
- 工具版本是否固定。
- 输出是否声明。
- 标准输出和错误输出是否需要保存。
- 退出码非零时是否让任务失败。
- CI 是否安装了该工具。
10.20 Build Service 与 Worker API
多个 Task 需要共享网络客户端、限流器或连接池时,可以使用 Build Service,而不是创建全局单例:
abstract class HttpService : BuildService<BuildServiceParameters.None>, AutoCloseable { override fun close() { // 释放共享资源 }}大量相互独立的工作可以考虑 Worker API。它适合将一个 Task 拆成隔离工作单元,并由 Gradle 管理并发。
选择原则:
- 一个 Task 内的简单动作:直接使用 Task Action。
- 多个 Task 共享有限资源:Build Service。
- 一个 Task 内有大量独立单元:Worker API。
- 不要用线程、全局可变变量和静态单例绕过 Gradle 的并行模型。
10.21 自定义 Task 的测试
自定义 Task 不应只通过完整项目构建间接验证。可以使用 Gradle TestKit 创建功能测试:
dependencies { testImplementation(gradleTestKit())}测试至少覆盖:
- 默认输入生成的输出。
- 输入变化是否触发重新执行。
- 输出目录是否正确。
- 空输入和缺失文件如何处理。
- 错误输入是否给出可读异常。
- 第二次运行是否
UP-TO-DATE或FROM-CACHE。 - 在不同操作系统和 JDK 下是否可用。
对于 Convention Plugin 和二进制 Plugin,优先使用 TestKit 通过真实 Gradle 进程验证任务图和构建输出。
10.22 Task 常见错误
- 任务注册了但没有被请求或依赖,因此没有执行。
- 使用
getByName导致配置阶段创建过多任务。 - 只声明任务顺序,没有声明真正的数据依赖。
- 忘记声明生成文件,导致任务总是重新执行或错误命中缓存。
- 将输出写入源码目录或其他任务的输出目录。
- 在配置阶段执行网络、外部进程和大规模文件扫描。
- 在 Task Action 中持有 Project、Configuration 或不可序列化对象,导致 Configuration Cache 失败。
- 使用
--exclude-task后误以为构建仍然完成了完整验证。 - 在聚合任务中手工调用其他 Task,绕过任务图。
- 通过时间戳、随机数或机器路径产生不稳定输出。
10.23 Task 诊断清单
.\gradlew.bat tasks --all.\gradlew.bat help --task customTask.\gradlew.bat customTask --dry-run.\gradlew.bat customTask --info.\gradlew.bat customTask --rerun-tasks.\gradlew.bat customTask --scan排查顺序:
- 任务是否由正确插件或脚本注册。
- 任务是否进入用户请求的 Task Graph。
- 任务依赖是否正确。
- 输入和输出是否完整声明。
- 任务是否因为
UP-TO-DATE、缓存、onlyIf或排除参数而跳过。 - Task Action 是否真正执行。
- 输出是否写入预期目录。
- 第二次执行是否获得正确状态。
11. 插件机制
11.1 插件提供什么
插件可以添加:
- Task。
- Extension。
- Configuration。
- Source Set。
- 约定和默认值。
- 发布组件和变体。
11.2 核心插件
plugins { java application `java-library` `maven-publish` jacoco}同一个项目不一定需要同时应用这些插件,应按模块职责选择。
11.3 外部插件
plugins { id("org.jetbrains.kotlin.jvm") version "2.3.20"}插件版本需要根据项目兼容矩阵选定,示例版本不能替代升级评估。
11.4 插件仓库
pluginManagement { repositories { gradlePluginPortal() mavenCentral() google() }}插件解析仓库和普通依赖仓库是两个独立概念。
11.5 apply false
根构建中声明版本但不应用:
plugins { id("org.jetbrains.kotlin.jvm") version "2.3.20" apply false}子项目按需应用:
plugins { id("org.jetbrains.kotlin.jvm")}11.6 plugins {} 与旧式 apply
现代项目优先使用 plugins {}。它更适合静态解析、插件版本管理和 Kotlin DSL 类型安全访问器。
旧式写法仍可能出现在历史工程:
apply(plugin = "java")不要在没有迁移计划的情况下混合多种插件应用方式。
11.7 插件的三种主要作用域
Gradle 插件可以作用于不同层级:
| 作用域 | 主要对象 | 典型用途 |
|---|---|---|
Settings 插件 | Settings | 项目发现、插件解析、仓库和构建全局策略 |
Project 插件 | Project | Task、Extension、Configuration、Source Set 和模块约定 |
Gradle/初始化逻辑 | Gradle | 企业级初始化、构建监听和全局服务 |
普通 Java、Kotlin、Android 和发布插件通常是 Project Plugin:
plugins { java}它会对当前 Project 添加任务、Configuration、Extension 和默认约定。不要把 Project Plugin 的配置写进 Settings,也不要在 Settings 中直接访问尚未创建的 Project 任务。
11.8 插件 ID、版本与实现
插件声明通常包含三个概念:
插件 ID:org.example.company-java插件版本:1.4.0插件实现:Plugin<Project> 或预编译脚本插件插件 ID 应保持稳定,因为项目脚本、插件门户、插件 marker 和文档都会引用它。插件版本用于选择实现版本,但不代表插件一定兼容当前 Gradle、JDK 或其他插件。
插件版本治理建议:
- 根 Settings 或 Version Catalog 统一管理。
- 子项目通过
apply false或插件别名按需应用。 - 不在多个模块中重复声明不同版本。
- 升级前查看插件发布说明和兼容矩阵。
- 生产项目避免使用动态插件版本。
11.9 插件解析过程
执行以下脚本时:
plugins { id("org.example.company-java") version "1.4.0"}Gradle 通常会经过以下步骤:
读取 plugins 块 ↓查询 pluginManagement 规则 ↓在插件仓库中解析插件 marker ↓找到插件实现模块 ↓加载插件类或预编译脚本 ↓调用 Plugin.apply(Project) ↓创建 Extension、Task、Configuration 和约定插件解析阶段和普通依赖解析阶段是相关但不同的过程:
pluginManagement.repositories负责插件解析。dependencyResolutionManagement.repositories负责项目依赖解析。- 插件版本不应在普通
dependencies块中声明。 - 插件解析失败时,先看 Settings 的
pluginManagement,再看普通依赖仓库。
11.10 pluginManagement 与解析规则
Settings 中统一配置插件仓库:
pluginManagement { repositories { gradlePluginPortal() mavenCentral() google() }}可以通过解析规则把插件 ID 映射到内部实现:
pluginManagement { resolutionStrategy { eachPlugin { if (requested.id.id == "org.example.company-java") { useModule("com.example:company-gradle-plugin:${requested.version}") } } }}企业内网使用私有插件仓库时,应明确仓库顺序、凭据、证书和插件 marker 配置。不要把插件密码写入 settings.gradle.kts。
11.11 apply false 的工程意义
apply false 表示声明插件版本但不把插件应用到当前 Project:
plugins { id("org.jetbrains.kotlin.jvm") version "2.3.20" apply false}子项目按需应用:
plugins { id("org.jetbrains.kotlin.jvm")}它适合:
- 根项目集中声明插件版本。
- 多个子项目按职责选择插件。
- 避免根项目获得不需要的任务和 Extension。
- 让插件版本在代码审查中有单一来源。
apply false 不等于插件没有被解析,也不等于子项目自动获得插件功能。子项目仍需要显式应用插件。
11.12 核心、社区、企业和本地插件
核心插件
Gradle 官方提供的插件通常可以直接使用短名称:
plugins { java application `java-library`}社区插件
社区插件通常使用完整 ID 和版本:
plugins { id("com.example.quality") version "1.0.0"}使用前要检查维护状态、发布来源、兼容矩阵、许可证、安全记录和是否支持当前 Gradle。
企业插件
企业插件适合封装:
- Java/Kotlin Toolchain。
- 统一编译参数。
- 仓库和依赖策略。
- 测试、覆盖率和静态检查。
- 发布、签名和版本约定。
- CI 专用任务和报告。
本地插件
开发阶段可以通过 Included Build 或 build-logic 使用本地插件:
pluginManagement { includeBuild("build-logic")}这比把大量逻辑复制进多个 build.gradle.kts 更容易演进。
11.13 Script Plugin、预编译脚本插件和二进制插件
| 类型 | 形式 | 适用场景 | 局限 |
|---|---|---|---|
| Script Plugin | apply(from = "common.gradle.kts") | 小型共享规则、迁移兼容 | 类型和依赖边界较弱 |
| 预编译脚本插件 | *.gradle.kts 位于插件源码目录 | Convention Plugin、组织规范 | 仍需构建插件项目 |
| 二进制插件 | Kotlin/Java/Groovy Plugin<Project> | 复杂逻辑、公共 API、发布 | 开发和测试成本更高 |
经验原则:
- 两三个模块共享的简单规则可以先用脚本插件。
- 中大型项目的公共约定优先使用预编译脚本插件。
- 需要公开 Extension、复杂服务、跨版本维护或独立发布时使用二进制插件。
- 不要把所有逻辑都堆进
buildSrc,必要时使用独立build-logicIncluded Build。
11.14 Convention Plugin
Convention Plugin 把“项目应该遵守的默认规则”固化为插件:
company.java-library-conventions├── 应用 java-library├── 配置 Java Toolchain├── 配置 JUnit Platform├── 配置编译参数├── 应用质量插件└── 注册统一验证任务settings.gradle.kts:
pluginManagement { includeBuild("build-logic")}模块脚本:
plugins { id("company.java-library-conventions")}Convention Plugin 应提供默认值和稳定约定,但不要隐藏模块真正重要的依赖和发布差异。模块特殊行为应在模块脚本中显式表达。
11.15 build-logic 结构
推荐结构:
build-logic/├── settings.gradle.kts├── build.gradle.kts└── src/main/kotlin/ ├── company.java-library-conventions.gradle.kts ├── company.kotlin-conventions.gradle.kts └── company.quality-conventions.gradle.ktsbuild-logic/build.gradle.kts:
plugins { `kotlin-dsl`}
repositories { gradlePluginPortal() mavenCentral()}company.java-library-conventions.gradle.kts:
plugins { `java-library`}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
tasks.withType<Test>().configureEach { useJUnitPlatform()}插件 ID 通常由预编译脚本文件名推导,例如 company.java-library-conventions.gradle.kts 对应 company.java-library-conventions。
11.16 Plugin Extension 设计
插件需要向消费者暴露配置时,应该创建明确的 Extension:
interface CompanyQualityExtension { val failOnWarning: Property<Boolean> val reportDirectory: DirectoryProperty}插件应用时注册 Extension,并提供默认值:
val extension = project.extensions.create<CompanyQualityExtension>("companyQuality")extension.failOnWarning.convention(false)extension.reportDirectory.convention( project.layout.buildDirectory.dir("reports/company-quality"))消费者配置:
companyQuality { failOnWarning.set(true)}Extension 设计原则:
- 属性类型使用
Property<T>、ListProperty<T>、SetProperty<T>、DirectoryProperty和RegularFileProperty。 - 使用
convention提供默认值,避免过早.get()。 - 不把 Project、Task 或 Configuration 作为公共配置属性暴露。
- 配置项命名稳定、描述清晰、错误信息可读。
- 只暴露消费者真正需要修改的内容。
11.17 插件之间的协作
一个插件需要在另一个插件应用后配置任务时,可以监听插件 ID:
pluginManager.withPlugin("java") { extensions.configure<JavaPluginExtension> { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) }
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }}不要假设插件应用顺序一定固定,也不要在插件加载时直接用 getByName 查找可能尚未创建的任务。使用 withPlugin、named 和 configureEach 可以降低插件之间的隐式耦合。
11.18 插件中的 Task 注册
插件应使用懒注册和类型安全任务:
abstract class VerifyProject : DefaultTask() { @get:Input abstract val projectName: Property<String>
@TaskAction fun verify() { logger.lifecycle("verified ${projectName.get()}") }}
tasks.register<VerifyProject>("verifyProject") { group = "verification" projectName.set(project.name)}插件不应:
- 在配置阶段执行网络请求。
- 修改所有项目的全局状态。
- 写入源码目录或其他插件的输出目录。
- 假设特定 IDE 或本机路径存在。
- 直接调用另一个 Task 的 Action。
- 使用没有输入输出声明的生成任务。
11.19 插件测试
插件测试应至少包含三层:
- 单元测试:测试 Extension 默认值、版本计算和纯函数。
- 功能测试:使用 Gradle TestKit 创建临时项目并执行真实 Gradle。
- 集成测试:验证插件与 Java、Kotlin、Android、发布或质量插件协作。
TestKit 示例依赖:
dependencies { testImplementation(gradleTestKit())}功能测试需要验证:
- 插件是否可以被解析和应用。
- 任务是否存在且进入正确任务图。
- Extension 默认值和显式配置是否生效。
- 输入变化是否触发任务重新执行。
- Configuration Cache 和 Build Cache 行为是否符合预期。
- 错误配置是否给出清晰提示。
11.20 插件发布
发布二进制插件通常需要:
- 稳定的插件 ID。
- 插件实现模块。
- Plugin Marker Artifact。
- Maven POM 或 Gradle Module Metadata。
- sources 和 javadoc(如适用)。
- 签名、校验和和版本说明。
- 与 Gradle、JDK 和其他插件的兼容矩阵。
发布到插件门户或企业仓库前,先在临时仓库中验证消费者:
plugins { id("com.example.company-java") version "1.0.0"}插件发布版本应不可变。不要覆盖已经被团队或外部消费者使用的同名版本。
11.21 插件解析和应用错误
Plugin was not found
检查:
- 插件 ID 是否拼写正确。
- 版本是否存在。
pluginManagement.repositories是否包含正确仓库。- 私有插件是否配置了 Plugin Marker 或解析规则。
- 当前 Gradle、JDK 和插件版本是否兼容。
Could not find method 或 Unresolved reference
通常表示插件没有应用、访问器不可用、DSL 类型不匹配或配置发生在错误作用域。先执行:
.\gradlew.bat tasks --all.\gradlew.bat properties.\gradlew.bat help --stacktrace插件版本冲突
根项目、子项目、Version Catalog、build-logic 和 buildSrc 可能分别声明插件版本。统一版本来源,并避免同一个插件在多个位置以不同版本解析。
插件应用后任务不存在
检查插件是否真的应用到当前 Project、任务是否因变体或条件而创建,以及任务是否使用了正确的项目路径。
插件导致 Configuration Cache 失败
检查插件是否在 Task Action 中捕获 Project、Gradle 或不可序列化对象,是否在配置阶段访问网络或执行文件 I/O,并优先升级到支持当前 Gradle 的版本。
11.22 插件选择清单
引入第三方插件前,检查:
- 插件 ID、版本和官方仓库。
- 最后发布时间和维护状态。
- Gradle、JDK、Kotlin、AGP 兼容性。
- 插件应用后新增了哪些任务和依赖。
- 是否会修改仓库、编译参数或发布行为。
- 是否支持 Configuration Cache、Build Cache 和并行构建。
- 是否收集或上传构建数据。
- 是否存在安全公告和供应链风险。
- 是否有测试、文档和回滚方案。
12. Java 与 Kotlin/JVM 项目
12.1 Java Application
plugins { application}
group = "com.example"version = "1.0.0"
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
application { mainClass = "com.example.App"}常用任务:
.\gradlew.bat classes.\gradlew.bat test.\gradlew.bat run.\gradlew.bat installDist.\gradlew.bat distZip12.2 Java Library
plugins { `java-library`}java-library 提供 api 与 implementation 的区分:
dependencies { api("org.example:public-model:1.0.0") implementation("org.example:internal-helper:1.0.0")}- 公共 API 的方法签名暴露某依赖类型时,考虑使用
api。 - 仅模块内部使用时,使用
implementation。 - 优先缩小 API 面,减少消费者重编译和依赖泄漏。
12.3 Java Toolchain
java { toolchain { languageVersion = JavaLanguageVersion.of(21) vendor = JvmVendorSpec.ADOPTIUM }}Toolchain 用于选择编译、测试和文档任务使用的 JDK。它比单纯依赖开发机 JAVA_HOME 更可复现,但 Gradle 自身仍需要一个受支持 JVM 启动。
只指定 Java 语言版本通常比同时固定供应商更灵活。只有确有环境一致性要求时再限制 Vendor。
12.4 sourceCompatibility 和 --release
仅设置源码级别不能完整保证不会使用更高版本 JDK API。Java 编译通常可使用 options.release:
tasks.withType<JavaCompile>().configureEach { options.release = 17}Toolchain 决定使用哪个编译器,--release 决定目标 Java 平台 API 和字节码级别。
12.5 Kotlin/JVM
plugins { kotlin("jvm") version "2.3.20" application}
kotlin { jvmToolchain(21)}
application { mainClass = "com.example.AppKt"}Kotlin、Gradle 和 Java 的版本需要共同核对。不要只看某一个插件是否“最新”。
12.6 Source Set
Java 插件默认提供:
src/main/javasrc/main/resourcessrc/test/javasrc/test/resources自定义 Source Set:
sourceSets { create("integrationTest") { java.srcDir("src/integrationTest/java") resources.srcDir("src/integrationTest/resources") compileClasspath += sourceSets.main.get().output runtimeClasspath += output + compileClasspath }}生产项目还需为新 Source Set 正确连接 Configuration、测试任务和生命周期任务,不能只创建目录。
12.7 Java 编译选项与字节码目标
Java 项目至少要区分三类版本约束:
- 运行 Gradle 的 JVM:负责启动 Gradle Daemon 和执行构建逻辑。
- 编译使用的 JDK:由 Java Toolchain 选择,提供
javac、javadoc和测试运行时。 - 产物兼容的 Java 平台:由
options.release等编译选项控制,决定字节码级别和可使用的标准库 API。
一个面向 Java 17 的项目可以这样配置:
plugins { `java-library`}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" options.release = 17 options.compilerArgs.addAll( listOf("-Xlint:all", "-parameters") )}这里使用 JDK 21 执行编译器,但通过 --release 17 生成 Java 17 兼容产物。实际项目中应先确认 Gradle、插件、编译器和运行环境对该版本组合的支持情况。
sourceCompatibility 和 targetCompatibility 只描述源码语法与字节码目标,不能单独替代 --release 对 Java 平台 API 的约束。需要发布给较低版本 JDK 使用时,优先采用 options.release,并在 CI 中用目标版本运行测试。
12.8 编译器警告、参数和编码
编译选项应尽量通过任务类型统一配置,而不是只修改某一个任务实例:
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" options.isDeprecation = true options.isWarnings = true options.compilerArgs.add("-Xlint:unchecked")}常见选项的含义如下:
options.encoding固定源码和资源处理过程中使用的字符集,避免开发机默认编码不同造成构建差异。-parameters保留方法参数名,某些反射框架、依赖注入框架和 Web 框架会使用它。-Xlint:all或更细粒度的-Xlint:unchecked可以提前暴露潜在问题,但升级 JDK 后可能出现新的警告。options.release应与项目实际兼容范围一致,不要为了“能编译”而随意降低版本。
警告策略应分阶段推进。老项目可以先收集并分类警告,再逐步启用更严格的检查;新项目可以在 CI 中将关键警告视为失败条件。
12.9 资源文件与构建时替换
Java 插件默认处理 src/main/resources 和 src/test/resources:
src/main/resources/├── application.properties└── logback.xml
src/test/resources/└── application-test.properties通过 processResources 可以对少量构建元数据进行替换:
tasks.processResources { filesMatching("application.properties") { expand( "applicationVersion" to project.version, "buildRevision" to providers.environmentVariable("GIT_COMMIT") .orElse("local") .get() ) }}对应资源文件可以写成:
application.version=${applicationVersion}build.revision=${buildRevision}使用 expand 时要注意:
- 只匹配需要替换的文件,避免误处理二进制资源。
- 资源中如果包含
${...},应确认它是 Gradle 占位符还是应用自身运行时占位符。 - 不要把密码、令牌和私钥通过资源替换写入 JAR;构建产物通常会被缓存、上传和分发。
- 可通过
tasks.processResources的输入和输出检查替换是否导致缓存失效。
12.10 生成源码与生成资源
代码生成应有明确的输出目录,并将生成任务声明为编译任务的前置任务:
val generatedSourcesDir = layout.buildDirectory.dir("generated/sources/demo/main")
val generateDemoSources by tasks.registering { outputs.dir(generatedSourcesDir) doLast { val outputDir = generatedSourcesDir.get().asFile outputDir.mkdirs() outputDir.resolve("com/example/GeneratedInfo.java").apply { parentFile.mkdirs() writeText( """ package com.example;
public final class GeneratedInfo { private GeneratedInfo() {} public static String version() { return "${project.version}"; } } """.trimIndent(), Charsets.UTF_8 ) } }}
sourceSets { named("main") { java.srcDir(generatedSourcesDir) }}
tasks.named<JavaCompile>("compileJava") { dependsOn(generateDemoSources)}代码生成任务需要重点保证:
- 生成目录位于
build/下,不污染源代码目录。 - 生成内容由声明的输入决定,输入改变时能重新生成。
- 任务输出使用
outputs.dir、outputs.file等 API 声明,支持增量构建和缓存。 - 不要在配置阶段直接执行外部生成器;应在任务动作中执行,并优先使用
Exec、Worker API 或对应插件提供的任务。 - 生成源码不要同时被手写源码目录再次收录,否则可能产生重复类。
12.11 Java 单元测试
Java 插件会创建 test Source Set 和 test 任务,但需要显式声明测试框架和平台:
plugins { java}
repositories { mavenCentral()}
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")}
tasks.test { useJUnitPlatform() testLogging { events("passed", "skipped", "failed") exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL showStandardStreams = false }}JUnit 5 测试类通常放在 src/test/java:
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class CalculatorTest { @Test void addsTwoNumbers() { assertEquals(5, new Calculator().add(2, 3)); }}常用测试命令:
.\gradlew.bat test.\gradlew.bat test --tests "com.example.CalculatorTest".\gradlew.bat test --tests "com.example.CalculatorTest.addsTwoNumbers".\gradlew.bat test --rerun代码块中的命令应保持为正常的 gradlew.bat 调用,不要混入终端控制字符。
12.12 测试过滤、日志与报告
调试单个失败测试时,优先缩小执行范围:
.\gradlew.bat test --tests "com.example.service.*".\gradlew.bat test --tests "*CalculatorTest".\gradlew.bat test --info也可以在构建脚本中统一测试日志:
tasks.withType<Test>().configureEach { useJUnitPlatform() testLogging { events("failed", "skipped") exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL showCauses = true showExceptions = true showStackTraces = true }}测试报告默认位于:
build/reports/tests/test/index.htmlbuild/test-results/test/--tests 只改变当前命令的筛选范围,不会修改测试任务的永久配置。对于不稳定测试,不应简单地反复使用 --rerun 掩盖问题;应记录复现条件、线程模型、外部依赖和随机种子。
12.13 集成测试与额外 Source Set
对数据库、HTTP 服务或消息中间件进行验证时,可以使用额外 Source Set。传统配置如下:
plugins { java}
val integrationTest by sourceSets.creating
configurations[integrationTest.implementationConfigurationName] .extendsFrom(configurations.testImplementation.get())configurations[integrationTest.runtimeOnlyConfigurationName] .extendsFrom(configurations.testRuntimeOnly.get())
dependencies { add(integrationTest.implementationConfigurationName, sourceSets.main.get().output) add(integrationTest.implementationConfigurationName, sourceSets.test.get().output)}
val integrationTestTask = tasks.register<Test>("integrationTest") { description = "运行集成测试。" group = "verification" testClassesDirs = integrationTest.output.classesDirs classpath = integrationTest.runtimeClasspath shouldRunAfter(tasks.test) useJUnitPlatform()}
tasks.check { dependsOn(integrationTestTask)}新项目也可以使用 jvm-test-suite 的声明式测试套件模型:
plugins { java `jvm-test-suite`}
testing { suites { val test by getting(JvmTestSuite::class) { useJUnitJupiter() } register<JvmTestSuite>("integrationTest") { dependencies { implementation(project()) } targets.all { testTask.configure { shouldRunAfter(test) } } } }}两种方式不要在同一个测试套件上重复创建相同的 Configuration 和任务。选择时可以遵循:
- 只需要单元测试时,使用 Java 插件自带的
test。 - 需要多个 JVM 测试套件时,优先评估
jvm-test-suite。 - 测试依赖外部服务时,明确服务启动、端口分配、数据清理和并行执行策略。
- 集成测试不应默认与单元测试共享不受控的全局状态。
12.14 测试夹具
多个模块需要复用测试辅助类时,可以使用 java-test-fixtures,而不是把测试代码放入生产 JAR:
plugins { `java-library` `java-test-fixtures`}
dependencies { testFixturesImplementation("org.assertj:assertj-core:3.27.3") testImplementation(testFixtures(project()))}测试夹具应保持稳定、最小化,并明确它只服务于测试。不要因为某个生产类在测试中常用,就默认把它移动到 fixtures;如果生产代码确实需要,应放在正常的 main API 或实现依赖中。
12.15 Java Library 的 API 边界
java-library 的核心价值不只是多了一个插件,而是将消费者可见的 API 依赖与实现依赖分开:
dependencies { api("org.example:public-api:1.0.0") implementation("org.example:internal-engine:1.0.0") compileOnly("org.example:optional-annotations:1.0.0") runtimeOnly("org.example:runtime-provider:1.0.0") testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")}判断规则:
api:库的公开类型、方法签名、父类或注解需要消费者看到该依赖。implementation:只在库内部使用,消费者不需要直接编译或声明该依赖。compileOnly:编译时需要,运行时由宿主或容器提供。runtimeOnly:运行时需要,但生产源码不直接编译引用其类型。testImplementation:只供测试编译和执行使用。
如果把所有依赖都放进 api,短期内可能“更容易编译”,长期会导致 API 泄漏、消费者依赖树膨胀和不必要的重编译。可以通过 dependencies、dependencyInsight 和发布后的 POM 验证边界是否符合预期。
12.16 Application Plugin 与运行参数
application 插件负责定义入口类、运行应用和创建分发包:
plugins { application}
application { mainClass = "com.example.App" applicationDefaultJvmArgs = listOf( "-Dfile.encoding=UTF-8", "-Dapp.environment=dev" )}运行时参数可以临时传入:
.\gradlew.bat run --args="--port 8080 --profile local"应用分发相关任务:
.\gradlew.bat run.\gradlew.bat installDist.\gradlew.bat distZip.\gradlew.bat distTar对应产物通常位于:
build/install/<project-name>/build/distributions/<project-name>.zipbuild/distributions/<project-name>.tarrun 适合本地开发验证;部署到服务器时,应优先审查 installDist 或压缩分发包中的启动脚本、依赖、JVM 参数和配置注入方式。不要把仅适用于本地的绝对路径写入分发脚本。
12.17 JAR、Sources JAR 与 Javadoc JAR
Java Library 发布时通常需要主 JAR、源码 JAR 和文档 JAR:
plugins { `java-library`}
java { withSourcesJar() withJavadocJar()}检查产物:
.\gradlew.bat jar.\gradlew.bat sourcesJar.\gradlew.bat javadocJar.\gradlew.bat assemble如果需要自定义 Manifest:
tasks.jar { manifest { attributes[ "Implementation-Title" to project.name, "Implementation-Version" to project.version, "Automatic-Module-Name" to "com.example.library" ] }}Manifest 中的 Automatic-Module-Name 不能替代真正的 module-info.java,也不能解决所有模块路径问题。库是否需要模块化,应结合消费者 JDK、依赖库模块名和发布策略决定。
12.18 Maven Publish 发布 Java 组件
将 Java 组件发布到 Maven 仓库时,使用 maven-publish:
plugins { `java-library` `maven-publish`}
group = "com.example"version = "1.0.0"
publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) pom { name = "Example Library" description = "一个示例 Java 库。" licenses { license { name = "The Apache License, Version 2.0" url = "https://www.apache.org/licenses/LICENSE-2.0.txt" } } } } }
repositories { maven { name = "localBuildRepo" url = layout.buildDirectory.dir("repo") } }}发布到本地构建目录并查看结果:
.\gradlew.bat publishMavenJavaPublicationToLocalBuildRepoRepositoryGet-ChildItem -Recurse build/repo正式仓库发布还需要处理:
- 版本策略:区分 release、snapshot 和预发布版本,避免重复覆盖不可变版本。
- 凭据注入:从环境变量或受保护的 CI Secret 读取,不写入
build.gradle.kts。 - 组件完整性:检查 POM、
.module元数据、主 JAR、Sources JAR 和 Javadoc JAR。 - 签名与供应链:按仓库要求启用签名,并保存发布审计信息。
- 发布前验证:在隔离仓库或临时版本上验证消费者能够解析和编译。
12.19 Java Module System
使用 Java Module System 时,在 src/main/java 下添加 module-info.java:
module com.example.library { exports com.example.api; requires org.slf4j;}Gradle 侧可以显式启用模块路径推断:
java { modularity.inferModulePath = true}模块化项目需要同时考虑:
exports控制可被其他模块访问的包,不等同于 Java 包目录本身存在。requires描述模块编译和运行时依赖。- 测试代码可能需要额外的
opens或测试模块配置。 - 普通 classpath 依赖、自动模块名和真正的显式模块在模块路径上表现不同。
- 第三方依赖没有稳定模块名时,升级库版本可能改变模块路径行为。
模块化迁移应从一个边界清晰的库开始,先在编译、测试和运行三个阶段分别验证,再扩展到多模块工程。
12.20 Java 与 Kotlin 混合项目
混合项目应统一 JDK、字节码目标和测试平台:
plugins { java kotlin("jvm") version "2.3.20"}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
kotlin { jvmToolchain(21)}
tasks.withType<JavaCompile>().configureEach { options.release = 17}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile>().configureEach { compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) }}混合项目的关键约束:
- Kotlin 的
jvmTarget与 Java 的targetCompatibility或options.release应一致。 - Kotlin 和 Java 源码之间存在相互引用时,应遵循 Kotlin Gradle Plugin 对编译顺序的支持方式,不要手动创建互相依赖的编译任务。
- Kotlin 编译器参数统一放在
compilerOptions中,避免在多个任务上重复配置旧式字符串参数。 - 使用
kapt、ksp或其他代码生成插件时,要单独核对插件版本、生成目录和增量构建兼容性。 - Java 与 Kotlin 的测试可以共用 JUnit Platform,但测试发现规则仍取决于测试框架和任务配置。
12.21 注解处理器与编译期工具
需要在编译阶段生成代码或检查注解时,应把处理器放在 annotationProcessor:
dependencies { compileOnly("org.example:annotations:1.0.0") annotationProcessor("org.example:processor:1.0.0")}不要把只在编译期使用的处理器放进 implementation 或 runtimeOnly。这样做会让运行时依赖树变大,也可能让处理器被错误地当成普通库加载。
排查注解处理问题时,可以按以下顺序检查:
- 处理器是否声明在正确的 Configuration。
- 处理器是否与当前 JDK 和 Java 编译器兼容。
- 生成目录是否被对应 Source Set 收录。
- 生成任务是否声明输入输出并正确连接到
compileJava或 Kotlin 编译任务。 - 清理构建后,生成源码能否独立重建。
12.22 完整的 JVM 应用示例
下面是一个包含 Toolchain、JUnit、资源替换和分发的最小应用构建脚本:
plugins { application}
repositories { mavenCentral()}
group = "com.example"version = "1.0.0"
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.12.2")}
application { mainClass = "com.example.App" applicationDefaultJvmArgs = listOf("-Dfile.encoding=UTF-8")}
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" options.release = 17}
tasks.test { useJUnitPlatform()}
tasks.processResources { filesMatching("application.properties") { expand("applicationVersion" to project.version) }}推荐的目录结构:
src/├── main/│ ├── java/com/example/App.java│ ├── java/com/example/Calculator.java│ └── resources/application.properties└── test/ ├── java/com/example/CalculatorTest.java └── resources/application-test.properties验证顺序:
.\gradlew.bat clean check.\gradlew.bat run.\gradlew.bat installDist.\gradlew.bat dependencies --configuration runtimeClasspath13. 依赖管理基础
13.1 依赖坐标
外部模块通常使用 Maven 坐标:
group:name:version例如:
dependencies { implementation("com.google.guava:guava:33.5.0-jre")}13.2 常见依赖声明
dependencies { implementation("com.google.guava:guava:33.5.0-jre") runtimeOnly("org.postgresql:postgresql:42.7.8")
testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher")}常见含义:
implementation:生产代码编译和运行需要,默认不暴露给库消费者的编译类路径。compileOnly:仅编译需要,运行时由容器或其他环境提供。runtimeOnly:只在运行时需要。annotationProcessor:Java 注解处理器。testImplementation:测试编译和运行需要。testRuntimeOnly:仅测试运行需要。
13.3 项目依赖
dependencies { implementation(project(":core"))}这不仅是文件路径引用。Gradle 会根据消费者请求的属性选择生产者的合适变体。
13.4 本地文件依赖
dependencies { implementation(files("libs/example.jar"))}或:
implementation(fileTree("libs") { include("*.jar")})本地 JAR 缺少可靠的模块元数据、传递依赖和版本治理能力,能发布到 Maven 仓库时应优先仓库依赖。
13.5 传递依赖
如果 A 依赖 B,B 又依赖 C,Gradle 通常会把 C 作为 A 的传递依赖解析。传递依赖降低声明成本,但也可能引入版本冲突、额外体积或安全风险。
排除特定传递依赖:
implementation("org.example:demo:1.0.0") { exclude(group = "commons-logging", module = "commons-logging")}排除依赖前要确认运行时确实不需要它。盲目排除经常把解析问题变成运行时 ClassNotFoundException。
13.6 动态版本和变化模块
避免:
implementation("org.example:demo:1.+")implementation("org.example:demo:latest.release")动态版本会使同一个提交在不同时间解析到不同结果,降低可复现性。SNAPSHOT 或 changing module 也需要明确缓存策略和发布约定。
13.7 刷新依赖
.\gradlew.bat build --refresh-dependencies该参数会重新检查依赖状态,不应该作为每次构建的默认选项。
13.8 Configuration 与依赖作用域
依赖声明写在 dependencies {} 中,但真正参与解析的是不同的 Configuration。Configuration 不只是“依赖列表”,还表达了依赖的用途、可见性、继承关系和参与的变体选择。
常见 Configuration 可以按用途理解:
| Configuration | 主要用途 | 是否通常参与生产运行时 |
|---|---|---|
api | 库公共 API 编译和消费者可见依赖 | 是 |
implementation | 当前项目内部编译和运行依赖 | 是 |
compileOnly | 当前项目编译需要、运行环境提供 | 否 |
runtimeOnly | 运行时需要、生产源码不直接编译引用 | 是 |
testImplementation | 测试源码编译和测试运行依赖 | 仅测试 |
testRuntimeOnly | 只供测试运行使用的引擎或运行时 | 仅测试 |
annotationProcessor | Java 编译期注解处理器 | 否 |
查看某个 Configuration 的完整依赖树:
.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencies --configuration testRuntimeClasspath.\gradlew.bat dependencies --configuration compileClasspath一个常见错误是把运行时实现声明为 compileOnly,或者把测试依赖写进 implementation。前者会导致应用在实际运行时缺少类,后者会把测试框架带进生产运行时。排查时应从实际任务使用的 Configuration 开始,而不是只看 build.gradle.kts 中的声明位置。
13.9 仓库声明与仓库顺序
仓库负责查找模块元数据和构件。推荐在 settings.gradle.kts 中统一声明仓库:
pluginManagement { repositories { gradlePluginPortal() mavenCentral() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() }}pluginManagement.repositories 用于解析插件请求;dependencyResolutionManagement.repositories 用于解析项目依赖。两者职责不同,不能因为都写了 mavenCentral() 就认为它们是同一组仓库。
仓库顺序会影响模块元数据和构件的查找。生产工程应尽量减少仓库数量,并避免多个仓库同时提供同一个坐标的不同内容。常见做法是:
- 公共依赖使用一个明确的公共仓库或企业代理仓库。
- 私有依赖通过企业仓库统一代理和审计。
- 不在项目脚本中随意添加个人临时仓库。
- 对只应来自某个仓库的组织设置内容过滤或独占内容。
- 变更仓库顺序时,重新检查解析结果和依赖校验元数据。
13.10 仓库内容过滤与独占内容
如果某些组织的模块只允许从内部仓库解析,可以配置内容过滤:
dependencyResolutionManagement { repositories { mavenCentral() maven { name = "companyRepository" url = uri("https://repo.example.com/maven/releases") content { includeGroupByRegex("com\\.example(\\..*)?") includeGroup("org.example.internal") } } }}更严格的场景可以使用独占内容:
dependencyResolutionManagement { repositories { exclusiveContent { forRepository { maven { name = "companyRepository" url = uri("https://repo.example.com/maven/releases") } } filter { includeGroupByRegex("com\\.example(\\..*)?") } } mavenCentral() }}内容过滤的价值包括减少无效网络请求、避免同一模块从错误仓库解析,以及降低仓库被污染或劫持时的影响范围。过滤规则应和组织的坐标命名约定一起维护。
13.11 Version Catalog
Version Catalog 用于集中管理依赖别名和版本。默认目录是 gradle/libs.versions.toml:
[versions]guava = "33.5.0-jre"junit = "5.12.2"
[libraries]guava = { module = "com.google.guava:guava", version.ref = "guava" }junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
[bundles]testing = ["junit-jupiter"]在 build.gradle.kts 中使用:
dependencies { implementation(libs.guava) testImplementation(libs.junit.jupiter) testImplementation(libs.bundles.testing)}Version Catalog 的边界:
- 它是依赖声明的集中入口,不是依赖解析策略本身。
- 它可以统一别名和版本,但不能替代平台、约束、锁定和依赖校验。
- 别名应表达业务或技术用途,避免把版本号编码进别名名称。
- 大型多项目工程应避免无意义地创建大量别名,优先围绕公共库、平台和测试套件组织。
- 升级版本时,应同时检查兼容矩阵、变更日志和锁文件,而不是只修改 TOML 中的字符串。
对于多个独立构建需要共享版本目录的情况,可以显式导入目录,但要注意路径耦合和目录命名冲突,避免让子项目依赖某个本地绝对路径。
13.12 Platform、BOM 与版本对齐
平台用于约束一组模块的兼容版本。以 Maven BOM 为例:
dependencies { implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.0")) implementation("org.springframework:spring-context") implementation("org.springframework:spring-web")}平台依赖通常只提供版本约束,不自动把平台中的所有模块添加到项目中。需要某个模块时仍要声明该模块本身。
也可以在多项目构建中定义自己的 Java Platform:
plugins { `java-platform`}
group = "com.example"version = "1.0.0"
javaPlatform { allowDependencies()}
dependencies { api(platform("org.junit:junit-bom:5.12.2")) constraints { api("com.google.guava:guava:33.5.0-jre") runtime("org.postgresql:postgresql:42.7.8") }}消费者项目:
dependencies { implementation(platform(project(":dependency-platform"))) implementation("com.google.guava:guava")}platform 和 enforcedPlatform 的差异需要谨慎理解:
platform提供普通约束,消费者仍可能通过其他声明覆盖或调整版本。enforcedPlatform将约束强制传递给消费者,容易形成隐式全局约束。- 业务库通常优先使用普通平台;只有确实需要强制一致性并能承担传递影响时,才考虑
enforcedPlatform。
13.13 Dependency Constraints
依赖约束表达“如果模块被使用,它应满足什么版本条件”,与直接声明依赖不同:
dependencies { constraints { implementation("org.example:shared-model:2.4.0") { because("所有模块使用同一版本的共享模型") } testImplementation("org.assertj:assertj-core:3.27.3") { version { strictly("3.27.3") } because("测试基线要求固定版本") } }}约束适合以下场景:
- 对传递依赖设定最低版本或推荐版本。
- 记录某个版本选择背后的安全、兼容或运行时原因。
- 在不主动引入模块的情况下,统一已存在模块的版本。
- 作为平台的一部分,为多个子项目提供一致的依赖策略。
约束不是排除冲突的万能手段。strictly、require、prefer 和 reject 的强度不同,应只使用满足目标所需的最小强度。约束变严格后,要同步检查所有消费者和测试任务。
13.14 Rich Version 与版本策略
Gradle 支持用 rich version 表达更精确的版本范围:
dependencies { implementation("org.example:demo") { version { strictly("[2.4, 3.0[") prefer("2.7.1") reject("2.6.0") } }}常见语义:
strictly:要求最终选择版本落在指定范围,超出范围通常视为解析失败。require:要求最低版本或指定范围,但允许其他约束进一步影响选择。prefer:在没有更强约束时提供偏好版本。reject:排除已知不兼容或有问题的版本。
版本范围应配合发布策略使用。对公共库使用过窄的严格范围可能阻碍消费者升级;对内部应用使用明确的严格范围,则有助于让升级失败尽早发生在构建阶段。
13.15 版本冲突与选择原因
当同一 Configuration 通过不同路径引入同一个模块的多个版本时,Gradle 需要进行冲突解决。不要只看最终版本,还要查看它为什么被选择:
.\gradlew.bat dependencyInsight ` --dependency guava ` --configuration runtimeClasspath在 PowerShell 中也可以写成一行:
.\gradlew.bat dependencyInsight --dependency guava --configuration runtimeClasspath依赖树中重点观察:
- 哪些直接依赖引入了目标模块。
- 哪个版本被选择,其他版本为何被拒绝或替换。
- 是否存在
strictly、平台、约束或强制版本影响。 - 选择的是哪个变体,使用了哪些属性和能力。
可在诊断阶段启用冲突失败:
configurations.configureEach { resolutionStrategy.failOnVersionConflict()}该配置适合发现隐藏冲突,但在大型工程中可能暴露大量历史问题。更稳妥的做法是先在关键 Configuration 或 CI 检查中启用,再逐步治理冲突来源。
13.16 Exclude 与依赖替换
排除传递依赖:
dependencies { implementation("org.example:demo:1.0.0") { exclude(group = "commons-logging", module = "commons-logging") }}排除是局部行为。同一个模块可能从其他依赖路径再次被引入,因此排除后必须检查完整依赖树和运行时测试。
依赖替换适合迁移旧坐标、测试本地项目或临时验证替代实现:
configurations.configureEach { resolutionStrategy.dependencySubstitution { substitute(module("org.example:old-api")) .using(module("org.example:new-api:2.0.0")) .because("旧坐标已迁移") }}替换规则应有明确的生命周期和删除计划。长期保留大量全局替换会让构建行为难以从依赖声明本身推断,也会增加升级和调试成本。
13.17 变体、属性与能力
现代 Gradle 依赖解析不是简单的“坐标对应一个 JAR”。同一个组件可以发布多个变体,例如:
- Java 编译变体和运行时变体。
- 不同 Java 版本或不同标准库的变体。
- 带有 Sources、Javadoc 或测试夹具的变体。
- Android、原生平台和其他插件定义的专用变体。
消费者会根据 Usage、Category、LibraryElements、目标 JVM 等属性选择兼容变体。项目依赖中使用 project(":core") 时,Gradle 会根据消费者 Configuration 请求生产者的匹配变体,而不是简单复制 core/build/libs 下的某个文件。
当看到 No matching variant 时,按以下顺序排查:
- 查看消费者使用的 Configuration,例如
compileClasspath还是runtimeClasspath。 - 检查生产者是否应用了正确插件并发布了预期组件。
- 对比双方的 JVM 版本、Usage、Category 和 Library Elements 属性。
- 使用
outgoingVariants查看生产者对外暴露的变体。 - 检查是否有自定义属性、Capabilities 或插件规则改变了选择。
.\gradlew.bat :core:outgoingVariants.\gradlew.bat :app:dependencies --configuration runtimeClasspathCapabilities 用于表达多个组件可能提供同一个能力,例如同一功能存在多个实现。出现 capability 冲突时,应显式选择实现或调整依赖声明,不要靠随机的仓库顺序解决。
13.18 Component Metadata Rules
外部模块的元数据可能不完整、过时或不符合当前项目的使用方式。Component Metadata Rules 可以在解析时修正状态、补充依赖或声明能力:
components { withModule<Any>("org.example:legacy-module") { allVariants { withDependencies { add("org.example:required-runtime:1.2.0") } } }}实际项目应优先使用官方发布的正确元数据;只有在无法立即修复上游或需要兼容历史模块时才编写规则。规则应:
- 限定到明确的模块坐标和版本范围。
- 说明修正原因和上游问题链接。
- 配套依赖树、运行时和发布消费者测试。
- 避免把业务代码中的依赖判断塞进全局规则。
13.19 依赖诊断命令
依赖问题首先使用内置报告,而不是手工查看缓存目录:
.\gradlew.bat dependencies --configuration compileClasspath.\gradlew.bat dependencyInsight --dependency slf4j --configuration runtimeClasspath.\gradlew.bat buildEnvironment.\gradlew.bat outgoingVariants.\gradlew.bat :app:dependencies --configuration testRuntimeClasspath命令选择建议:
dependencies:查看完整依赖树和传递路径。dependencyInsight:解释某个模块的版本选择和原因。buildEnvironment:查看构建逻辑自身的插件和类路径依赖。outgoingVariants:查看当前项目可以提供给消费者的变体。--info或--debug:在需要时查看仓库请求、缓存和解析过程。
诊断输出应保存到构建记录或问题单中,尤其是升级依赖、处理安全漏洞和排查 CI 与本地差异时。
13.20 Dependency Locking
依赖声明描述“允许解析什么”,锁文件记录“本次解析实际选择了什么”。启用锁定:
dependencyLocking { lockAllConfigurations()}生成或更新锁文件:
.\gradlew.bat dependencies --write-locks.\gradlew.bat build --write-locks.\gradlew.bat build --update-locks org.example:demo锁定策略应纳入版本控制,并明确以下规则:
- 锁文件是构建输入,不应加入
.gitignore。 - 依赖声明、平台和约束变更后,应审查锁文件差异。
- 只更新目标模块时,使用
--update-locks,避免无意中升级整个依赖图。 - 升级后运行完整测试、打包和安全检查。
- 多个环境若使用不同 Configuration,应确认每个需要可复现的 Configuration 都被锁定。
锁定不是永久冻结。它的价值在于让升级成为显式、可审查和可回滚的变更。
13.21 Dependency Verification
依赖锁定解决版本选择问题,依赖验证解决构件完整性问题。可以生成 SHA-256 校验元数据:
.\gradlew.bat --write-verification-metadata sha256 build在严格模式下执行验证:
.\gradlew.bat build --dependency-verification=strict校验元数据通常位于:
gradle/verification-metadata.xml依赖验证工作流:
- 在可信网络和可信构建环境中生成或更新校验元数据。
- 审查新增模块、版本、仓库和校验和变更。
- 将校验文件提交到版本控制。
- 在 CI 和发布构建中使用严格验证。
- 依赖升级时确认新构件来自预期仓库,并记录变更原因。
校验和只能证明当前构件与记录一致,不能单独证明构件内容本身是安全的。仍需结合供应商、漏洞数据库、签名、许可证和代码审查。
13.22 缓存、离线模式与可复现构建
Gradle 会缓存依赖元数据和构件。常用选项:
.\gradlew.bat build --offline.\gradlew.bat build --refresh-dependencies.\gradlew.bat build --info--offline只使用本地已有缓存,适合验证离线构建能力,但缺少缓存时会失败。--refresh-dependencies强制重新检查依赖状态,适合处理仓库元数据变化或缓存异常,不适合每次构建都使用。--info可以帮助判断请求、缓存命中和解析阶段,但不应把完整敏感日志公开到工单或 CI 输出。
可复现构建还需要统一:
- Gradle Wrapper 和插件版本。
- Java Toolchain 与操作系统相关工具。
- 仓库地址、凭据和仓库内容规则。
- 版本目录、平台、约束与锁文件。
- 依赖验证元数据。
- 生成文件、时间戳、文件排序和构建环境变量。
13.23 依赖升级与治理流程
推荐把依赖升级拆成可审查的小步骤:
- 确认升级目标、当前版本、受影响的 Configuration 和直接来源。
- 阅读上游发布说明、兼容要求、安全公告和迁移指南。
- 修改 Version Catalog、平台或直接声明,不要直接编辑缓存。
- 使用
dependencyInsight检查目标版本是否真的被选择。 - 更新锁文件和验证元数据。
- 运行单元测试、集成测试、静态检查、打包和许可证检查。
- 对比 JAR、POM、Gradle Module Metadata 和运行时依赖树。
- 保留回滚方式和升级理由。
对于安全升级,不应只把某个传递依赖强行 force 到高版本就结束。应确认高版本与直接依赖、平台、Java 版本和运行时行为兼容,并在后续版本中移除临时规则。
13.24 多项目依赖治理
多项目工程可以通过约定插件或共享平台统一依赖治理:
subprojects { repositories { mavenCentral() }
configurations.configureEach { resolutionStrategy.failOnVersionConflict() }}但不建议把大量业务依赖和解析例外直接塞进根项目的 subprojects 块。更可维护的分层方式是:
settings.gradle.kts:统一插件仓库、依赖仓库和仓库模式。- Version Catalog:统一别名和常用版本入口。
java-platform:统一组件版本约束和 BOM 对齐。- Convention Plugin:统一 Java、测试、质量检查和发布约定。
- 各子项目:只声明自身真正使用的模块和特殊配置。
- 锁文件与验证元数据:记录每个构建实际接受的依赖结果。
依赖治理应尽量靠声明式模型表达。越多依赖运行时条件、全局替换和任务顺序,越难解释构建结果,也越难迁移到配置缓存和并行构建。
13.25 依赖管理综合示例
下面示例展示 Version Catalog、Java Platform、应用模块和测试配置的组合方式。
gradle/libs.versions.toml:
[versions]junit = "5.12.2"guava = "33.5.0-jre"
[libraries]junit = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }guava = { module = "com.google.guava:guava", version.ref = "guava" }dependency-platform/build.gradle.kts:
plugins { `java-platform`}
dependencies { constraints { api(libs.guava) api("org.junit.jupiter:junit-jupiter:${libs.versions.junit.get()}") }}app/build.gradle.kts:
plugins { application}
dependencies { implementation(platform(project(":dependency-platform"))) implementation(libs.guava) testImplementation(libs.junit)}
tasks.test { useJUnitPlatform()}推荐的验证命令:
.\gradlew.bat :app:dependencies --configuration runtimeClasspath.\gradlew.bat :app:dependencyInsight --dependency guava --configuration runtimeClasspath.\gradlew.bat dependencies --write-locks.\gradlew.bat --write-verification-metadata sha256 check.\gradlew.bat check示例中的版本号仅用于说明配置方式。实际工程应结合当前项目的 Java 版本、插件兼容矩阵、仓库策略和安全要求选择版本。
13.26 依赖管理常见问题
1. 声明了依赖但编译器仍找不到类
先确认依赖声明所在的 Configuration 是否被当前任务使用,再检查坐标、仓库、变体和依赖是否被排除。implementation、runtimeOnly 和 testImplementation 并不等价。
2. 本地能构建,CI 解析失败
比较 Wrapper、JDK、仓库凭据、仓库内容过滤、代理、缓存和锁文件。使用 --offline 只能验证缓存是否完整,不能解决 CI 没有依赖的问题。
3. dependencyInsight 显示版本不是声明的版本
检查传递依赖、平台、约束、rich version、锁文件和替换规则。最终选择版本是多个声明共同作用的结果。
4. 出现 No matching variant
检查消费者 Configuration 和生产者的 outgoing variants,重点关注 Usage、Category、目标 JVM、Library Elements 和自定义属性。
5. 使用 exclude 后运行时报类缺失
说明被排除的模块仍可能是某条运行时路径的必要依赖。恢复依赖后,确认真正需要它的组件,再通过直接声明、平台或升级上游模块解决问题。
6. 依赖升级后锁文件变化过大
不要直接接受全部差异。确认是否使用了全量 --write-locks、是否有动态版本、是否发生平台升级或仓库元数据变化;必要时用 --update-locks 精确更新目标模块。
14. Configuration 与变体
14.1 三种角色
Configuration 可以具有不同角色:
- Declarable:用于声明依赖,如
implementation。 - Resolvable:用于解析文件,如
runtimeClasspath。 - Consumable:向其他项目发布变体,如
runtimeElements。
现代 Gradle 鼓励一个 Configuration 只承担清晰角色,而不是同时声明、解析和发布。
14.2 常见类路径
implementation ↓compileClasspathruntimeClasspath测试通常扩展生产代码相关依赖:
testImplementationtestCompileClasspathtestRuntimeClasspath14.3 查看 Configuration
.\gradlew.bat dependencies.\gradlew.bat dependencies --configuration runtimeClasspath14.4 自定义 Configuration
val codegen by configurations.creating { isCanBeDeclared = true isCanBeResolved = false isCanBeConsumed = false}如需解析,建议创建独立的 Resolvable Configuration,并通过 extendsFrom 继承声明,而不是让同一个对象承担所有职责。
14.5 变体感知解析
Gradle 可根据属性选择合适变体,例如:
- 用途是编译还是运行。
- JVM 版本。
- Library Elements 是 JAR 还是 classes。
- Android 的 Build Type、Flavor 和目标平台。
出现 “No matching variant” 时,通常不是简单的“找不到 JAR”,而是消费者要求与生产者提供的属性不匹配。
14.6 Configuration 的声明、解析与消费边界
Configuration 的三个角色最好显式分开:
val toolDependencies by configurations.dependencyScope("toolDependencies")
val toolClasspath by configurations.resolvable("toolClasspath") { extendsFrom(toolDependencies)}
val publishedTools by configurations.consumable("publishedTools") { extendsFrom(toolDependencies)}如果项目或插件版本尚未提供上述便捷方法,也可以使用兼容性更好的写法:
val toolDependencies by configurations.creating { isCanBeDeclared = true isCanBeResolved = false isCanBeConsumed = false}
val toolClasspath by configurations.creating { isCanBeDeclared = false isCanBeResolved = true isCanBeConsumed = false extendsFrom(toolDependencies)}判断一个 Configuration 是否设计合理,可以问三个问题:
- 谁向它声明依赖?
- 哪个任务或解析过程消费它?
- 哪个项目或变体向外暴露它?
如果同一个 Configuration 同时被脚本当作声明入口、文件解析入口和发布变体使用,后续很容易出现属性污染、依赖泄漏和无法解释的变体选择。
14.7 extendsFrom 与依赖继承
extendsFrom 表示一个 Configuration 继承另一个 Configuration 中的依赖声明:
val codegenDependencies by configurations.creating { isCanBeDeclared = true isCanBeResolved = false}
val codegenClasspath by configurations.creating { isCanBeResolved = true isCanBeConsumed = false extendsFrom(codegenDependencies)}
dependencies { add(codegenDependencies.name, "org.example:codegen:1.2.0")}这里的继承关系不是文件复制,也不是把两个 Configuration 合并成一个新变体。它主要影响依赖声明和解析结果;最终文件仍由被解析的 Configuration 根据自身属性和依赖图计算。
常见继承关系:
testImplementation ↓testCompileClasspath
testRuntimeOnly ↓testRuntimeClasspath
codegenDependencies ↓codegenClasspath设计继承关系时注意:
- 只继承确实需要的依赖,避免把生产运行时依赖带进工具类路径。
- 不要通过多层
extendsFrom隐藏关键依赖来源。 - 解析 Configuration 不应反过来成为声明 Configuration 的父级。
- 修改继承关系后,用
dependencies --configuration <name>检查完整结果。
14.8 声明依赖与解析文件
声明依赖和解析文件应由不同对象承担:
val generatorDependencies by configurations.creating { isCanBeDeclared = true isCanBeResolved = false}
val generatorClasspath by configurations.creating { isCanBeDeclared = false isCanBeResolved = true isCanBeConsumed = false extendsFrom(generatorDependencies)}
dependencies { add(generatorDependencies.name, "org.example:generator:1.0.0")}
tasks.register<JavaExec>("runGenerator") { classpath = generatorClasspath mainClass = "org.example.Generator" args("--input", layout.projectDirectory.dir("schema").asFile.absolutePath)}不要在配置阶段直接访问 generatorClasspath.files:
// 不推荐:配置阶段就解析依赖val files = generatorClasspath.files应该让任务在执行阶段使用类路径,或者将其作为任务的输入:
tasks.named<JavaExec>("runGenerator") { classpath(generatorClasspath)}这样可以保留配置阶段的惰性,也更容易兼容 Configuration Cache 和依赖解析延迟执行。
14.9 Incoming 与 Outgoing 视角
Configuration 有两个互补视角:
- Incoming:当前项目从依赖图中请求什么,并解析出哪些文件或变体。
- Outgoing:当前项目向消费者提供什么依赖、属性、Capabilities 和构件。
查看 Incoming 依赖:
.\gradlew.bat :app:dependencies --configuration compileClasspath.\gradlew.bat :app:dependencyInsight --dependency log4j --configuration runtimeClasspath查看 Outgoing 变体:
.\gradlew.bat :library:outgoingVariants在多项目构建中,app 的 implementation(project(":library")) 会向 library 请求适合当前消费者的可消费变体。排查问题时应同时查看消费者的 Incoming 和生产者的 Outgoing,不能只检查某一侧。
14.10 Attributes 基础
Attributes 是变体感知解析的输入。消费者表达需求,生产者的各个变体表达能力,Gradle 根据属性匹配合适的变体。
常见属性包括:
| 属性 | 典型值 | 作用 |
|---|---|---|
Usage | java-api、java-runtime | 表示编译用途或运行时用途 |
Category | library、platform、documentation | 区分普通库、平台和文档 |
LibraryElements | jar、classes | 区分 JAR 文件和 classes 目录 |
Bundling | external、embedded | 表示依赖是否被内嵌 |
TargetJvmVersion | 8、17、21 | 表示目标 JVM 版本 |
| 自定义属性 | dev、prod、linux | 表达项目自己的环境或平台维度 |
Java 插件会自动为标准变体配置许多属性,因此通常不需要手动设置 Usage。只有在自定义变体、跨语言插件或发布特殊组件时,才需要显式建模。
14.11 为消费者设置属性请求
可以为一个可解析 Configuration 设置属性:
import org.gradle.api.attributes.Categoryimport org.gradle.api.attributes.Usage
configurations.named("runtimeClasspath") { attributes { attribute( Usage.USAGE_ATTRIBUTE, objects.named(Usage::class.java, Usage.JAVA_RUNTIME) ) attribute( Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.LIBRARY) ) }}实际项目中优先让 Java、Kotlin、Android 或 Native 插件自动配置标准属性。手动覆盖标准属性可能导致:
compileClasspath被错误地当成运行时类路径。- 项目依赖无法匹配生产者的标准变体。
- JAR、classes 目录和平台变体之间发生错误选择。
- 某个任务能解析,另一个任务却出现
No matching variant。
只有当你清楚消费者的目标和生产者的变体模型时,才应覆盖插件已经配置好的属性。
14.12 变体匹配的两个阶段
Gradle 的变体选择可以粗略分为两个阶段:
- 兼容性判断:判断生产者候选值能否满足消费者请求。例如,消费者要求 Java 17 时,Java 8 或 Java 17 产物可能兼容,而 Java 21 产物通常不能直接作为 Java 17 编译输入。
- 消歧选择:多个候选都兼容时,根据属性的优先级和默认策略选择最合适的一个。
因此,出现匹配问题时不能只看“有没有这个属性”,还要区分:
- 候选完全不兼容,导致没有匹配变体。
- 候选都兼容,但没有足够信息消歧,导致选择歧义。
- 候选选择成功,但选中的构件不满足运行时预期。
诊断时应先查看完整错误信息中的 Consumer attributes、Producer variants 和每个候选被拒绝的原因。
14.13 自定义 Attribute
当项目确实存在环境、平台或功能维度时,可以定义自定义属性:
val environment = Attribute.of( "com.example.environment", String::class.java)
dependencies { attributesSchema { attribute(environment) }}
configurations.named("runtimeClasspath") { attributes { attribute(environment, "prod") }}生产者可以为不同变体设置属性:
val prodRuntimeElements by configurations.creating { isCanBeConsumed = true isCanBeResolved = false attributes { attribute(environment, "prod") }}自定义属性要满足:
- 名称具有组织或插件级命名空间,避免与其他插件冲突。
- 每个变体对该属性都有明确的含义。
- 消费者请求值和生产者提供值使用同一类型和同一套约定。
- 兼容性与消歧规则经过测试,不依赖 Configuration 的注册顺序。
- 如果标准 Gradle 属性已经能表达需求,不要重复创建自定义属性。
14.14 Attribute Compatibility 与 Disambiguation Rule
当默认匹配策略不能表达项目规则时,可以注册兼容性规则或消歧规则。下面是一个“消费者要求 prod 时优先选择 prod”的简化消歧示例:
abstract class EnvironmentDisambiguationRule : AttributeDisambiguationRule<String> { override fun execute(details: MultipleCandidatesDetails<String>) { if (details.candidateValues.contains("prod")) { details.closestMatch("prod") } }}
val environment = Attribute.of( "com.example.environment", String::class.java)
dependencies { attributesSchema { attribute(environment) { disambiguationRules.add(EnvironmentDisambiguationRule::class.java) } }}规则应该尽量小而确定:
- 只处理自己定义的属性。
- 不通过读取文件、网络或环境变量改变匹配结果。
- 对候选为空、只有一个和多个候选分别有明确行为。
- 为规则编写项目级测试,验证成功选择和预期失败两类场景。
- 不要用规则掩盖生产者发布了错误属性或缺失变体的问题。
14.15 Java Library 的标准变体
应用 java-library 后,项目通常会暴露编译和运行时相关变体:
apiElementsruntimeElementsmainSourceElements
sourcesElementsjavadocElements这些变体并不是简单的任务名称,而是带有属性、依赖和构件的可消费视图。消费者使用 compileClasspath 时通常请求 API 变体,使用 runtimeClasspath 时通常请求运行时变体。
当库使用 api 与 implementation 时,变体中暴露的依赖也不同:
apiElements暴露公共 API 所需的依赖。runtimeElements可以包含运行时实现依赖。- 消费者不应因为某个实现依赖存在于生产者本地类路径,就自动把它当成自己的编译 API。
查看 Java Library 的对外模型:
.\gradlew.bat outgoingVariants.\gradlew.bat dependencies --configuration apiElements.\gradlew.bat dependencies --configuration runtimeElements14.16 Feature Variant
Java Library 可以为可选功能注册 Feature Variant:
plugins { `java-library`}
java { registerFeature("databaseSupport") { usingSourceSet(sourceSets.create("databaseSupport")) }}
dependencies { add("databaseSupportImplementation", "org.example:database-driver:1.0.0")}对应目录可以是:
src/databaseSupport/java/src/databaseSupport/resources/Feature Variant 适合将可选能力作为独立变体发布,让消费者显式选择该能力,而不是默认把所有可选依赖塞进主库。
设计 Feature Variant 时要确认:
- 功能源码只引用该功能自己的依赖和主 Source Set 的公开 API。
- 发布仓库支持对应的 Gradle Module Metadata。
- Maven POM 消费者是否能获得等价行为,必要时提供兼容方案。
- 消费者的选择方式和文档足够清晰。
- 测试覆盖主变体和每个 Feature Variant 的编译、运行与发布结果。
14.17 Capabilities 与组件冲突
Capability 表示一个组件提供的能力。多个组件声明同一个 capability 时,消费者可能遇到能力冲突,需要显式选择实现。
适合使用 Capability 的场景包括:
- 同一库存在互斥的实现模块。
- 新坐标与旧坐标实际上提供相同功能。
- 一个组件的不同变体提供可替代的实现。
- 插件希望让消费者选择一个明确的实现,而不是把两个实现同时放进类路径。
排查 capability 冲突时,重点检查:
- 哪些组件声明了同一个 capability。
- 当前消费者请求了哪个 Configuration 和变体。
- 是否有平台、约束或选择规则改变了组件选择。
- 是否应该保留一个实现,或让消费者显式声明选择。
不要通过随意排除其中一个模块来解决能力冲突,除非你已经确认另一个模块完整提供所需功能。
14.18 自定义构件与 Outgoing Artifacts
除了 JAR,项目也可以向外发布生成的报告、Schema 或其他构件:
val schema by configurations.consumable("schema") { isCanBeResolved = false attributes { attribute( Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, "documentation") ) }}
val generateSchema by tasks.registering { val output = layout.buildDirectory.file("schema/model.json") outputs.file(output) doLast { output.get().asFile.apply { parentFile.mkdirs() writeText("{\"version\":\"1\"}") } }}
artifacts { add(schema.name, generateSchema)}生产者发布构件时,应同时考虑:
- 构件的类型、属性和用途是否表达清楚。
- 生成任务是否声明输入输出并在消费前完成。
- 构件是否适合缓存和并行执行。
- 消费者是否通过变体选择而不是硬编码
build/路径获取它。
跨项目共享构件时,优先使用项目依赖和可消费 Configuration,不要让一个项目直接读取另一个项目的内部输出目录。
14.19 跨项目变体选择
一个典型的多项目结构:
:app:library:dependency-platformapp 依赖 library:
dependencies { implementation(project(":library"))}Gradle 会根据 app 当前使用的 Configuration 向 library 请求匹配变体。例如:
app:compileClasspath通常请求library的 API 编译能力。app:runtimeClasspath通常请求library的运行时能力。app如果请求平台,则应匹配dependency-platform的平台属性。app如果请求文档或源码,则应匹配对应的文档或源码变体。
如果通过 project(path = ":library", configuration = "runtimeElements") 强行指定 Configuration,会绕过部分变体感知模型。除非有明确的兼容原因,否则应优先使用普通项目依赖。
14.20 No matching variant 排错流程
遇到如下错误时:
No matching variant of project :library was found.建议按以下顺序排查:
- 确认依赖方向和项目路径没有写错。
- 运行
:library:outgoingVariants,确认生产者确实暴露变体。 - 检查消费者 Configuration,确认是编译、运行、平台还是文档请求。
- 对比错误信息中的 Consumer attributes 和 Producer attributes。
- 检查目标 JVM 版本是否兼容。
- 检查
Category、Usage、LibraryElements和自定义属性。 - 检查生产者是否错误地把 Configuration 设置成不可消费。
- 检查是否有插件、Capabilities 或自定义规则改变了选择。
- 清理并重新运行最小任务,排除旧缓存和旧生成物干扰。
有时错误来自“变体太多且无法消歧”,而不是“变体太少”。这种情况应补充明确的消费者属性或消歧规则,而不是复制一个新的 Configuration。
14.21 Configuration 与变体的测试
自定义插件或构建逻辑应测试 Configuration 和变体行为,而不是只测试最终 JAR 是否存在:
val verifyOutgoingVariants by tasks.registering { doLast { val outgoing = configurations .filter { it.isCanBeConsumed } .map { it.name } check("runtimeElements" in outgoing) { "缺少 runtimeElements 可消费变体" } }}更可靠的测试方式是使用 Gradle TestKit 创建临时多项目工程,验证:
- 消费者能否解析预期变体。
- 错误属性是否得到预期的失败信息。
- Feature Variant 和测试夹具是否能被正确消费。
- 发布后从仓库消费时,Gradle Module Metadata 和 POM 是否都符合预期。
- 自定义 Attribute Rule 是否在不同候选集合下稳定选择。
14.22 Configuration 与变体设计原则
推荐遵循以下原则:
- 声明、解析和消费角色分离。
- 让插件创建标准 Configuration,项目只补充必要依赖和属性。
- 通过
extendsFrom表达有限、可解释的依赖继承。 - 优先使用标准属性,只有存在真实维度时才创建自定义属性。
- 不通过读取其他项目的
build/目录实现跨项目依赖。 - 不用全局强制版本、全局排除和硬编码 Configuration 名称掩盖模型问题。
- 为每个自定义变体定义名称、属性、依赖、构件和消费方式。
- 发布库时同时检查 Gradle Module Metadata、POM 和实际消费者行为。
- 使用
dependencies、dependencyInsight和outgoingVariants记录诊断证据。
15. 版本冲突与依赖诊断
15.1 查看依赖树
.\gradlew.bat dependencies --configuration runtimeClasspath多项目中指定模块:
.\gradlew.bat :app:dependencies --configuration runtimeClasspath15.2 dependencyInsight
.\gradlew.bat dependencyInsight --dependency guava --configuration runtimeClasspath它可以说明:
- 依赖由谁引入。
- 哪个版本被选中。
- 是否发生冲突解决。
- 是否由约束、平台或强制规则影响。
15.3 版本冲突
默认冲突解决通常会从候选版本中选择一个版本,但“能解析”不等于“运行时兼容”。典型后果:
NoSuchMethodErrorNoClassDefFoundErrorClassCastException可在关键 Configuration 上启用冲突失败:
configurations.configureEach { resolutionStrategy.failOnVersionConflict()}大型工程应评估启用范围,先处理现有冲突再作为门禁。
15.4 优先治理手段
推荐顺序:
- 使用官方 BOM 或 Gradle Platform 对齐版本。
- 使用 Dependency Constraint 描述兼容要求。
- 升级直接依赖,让传递依赖自然收敛。
- 精确排除错误依赖并显式补回正确依赖。
- 最后才考虑
force等强制规则。
强制版本可能掩盖真实不兼容:
configurations.configureEach { resolutionStrategy.force("org.example:lib:2.0.0")}不要仅因为依赖树“看起来整齐”就使用 force。
15.5 常用诊断组合
.\gradlew.bat :app:dependencies --configuration runtimeClasspath.\gradlew.bat :app:dependencyInsight --dependency jackson --configuration runtimeClasspath.\gradlew.bat build --stacktrace --info15.6 依赖解析的基本流程
一次依赖解析可以拆成几个阶段:
- 根据任务选择要解析的 Configuration,例如
compileClasspath或runtimeClasspath。 - 收集直接依赖、项目依赖、平台、约束和传递依赖。
- 从仓库获取模块元数据、Gradle Module Metadata、POM 或 Ivy 描述。
- 根据模块坐标合并同一组件的候选版本。
- 应用版本约束、平台、替换、排除和组件选择规则。
- 根据消费者属性选择生产者的兼容变体。
- 解析最终构件、执行依赖验证,并将结果交给任务使用。
因此,“依赖树中的版本不等于声明的版本”是正常现象。声明只是解析输入,最终结果还会受到传递依赖、约束、平台、锁文件、仓库元数据和变体属性影响。
诊断问题时,应先记录任务和 Configuration:
.\gradlew.bat :app:dependencies --configuration compileClasspath.\gradlew.bat :app:dependencies --configuration runtimeClasspath.\gradlew.bat :app:dependencyInsight --dependency org.example:demo --configuration runtimeClasspath不要使用不相关的 Configuration 解释目标任务。例如,compileClasspath 上的结果不能直接证明 runtimeClasspath 一定使用相同版本或相同变体。
15.7 直接依赖、传递依赖与路径
依赖树中的每一条路径都说明了一个引入来源:
app└── service:1.0.0 └── logging-api:2.0.0 └── annotations:1.3.0排查时区分三类依赖:
- 直接依赖:当前项目在构建脚本中明确声明。
- 传递依赖:由直接依赖或其他传递路径引入。
- 约束依赖:不一定主动引入模块,但会影响模块存在时的版本选择。
如果同一个模块从多条路径进入依赖图,dependencies 会展示路径,dependencyInsight 更适合回答“为什么最终选择这个版本”。
.\gradlew.bat :app:dependencyInsight ` --dependency logging-api ` --configuration runtimeClasspath在 PowerShell 中也可以使用一行命令:
.\gradlew.bat :app:dependencyInsight --dependency logging-api --configuration runtimeClasspath不要仅凭依赖树的缩进深度判断重要性。一个较深的传递依赖也可能被应用代码直接加载,反之,某个直接声明的依赖也可能只用于编译。
15.8 Selection Reason 与选择证据
依赖诊断中的选择原因是最重要的证据之一。常见原因包括:
By conflict resolution:多个版本发生冲突,解析器选择了一个版本。By constraint:版本受到 Dependency Constraint 影响。By ancestor:依赖来自某个上游路径。By platform:平台或 BOM 提供了版本对齐。Forced:使用了force或强制平台。Rejected:版本被 rich version 或组件选择规则拒绝。Selected by rule:自定义解析规则改变了选择。
使用 dependencyInsight 时应完整阅读以下信息:
- 目标模块的所有候选版本。
- 每个候选版本的引入路径。
- 最终版本的选择原因。
- 参与选择的 Configuration 和变体属性。
- 是否存在锁文件、平台或验证元数据的影响。
把 dependencyInsight 输出保存到升级记录中,可以让后续维护者知道版本不是“偶然变成这样”,而是由哪条规则决定的。
15.9 版本冲突的根因分类
版本冲突可以分成几种根因:
同一组件的直接版本不一致
dependencies { implementation("org.example:core:1.0.0") implementation("org.example:adapter:2.0.0")}传递依赖带入旧版本
app -> new-client:2.0.0 -> shared-api:3.0.0app -> old-plugin:1.0.0 -> shared-api:2.0.0平台或约束与直接声明冲突:平台统一版本,但某个项目又声明了不同版本。
变体或模块元数据不一致:同一组件的 POM 与 Gradle Module Metadata 描述不同,导致消费者看到不同的依赖或变体。
仓库内容不一致:相同坐标从不同仓库解析到不同元数据或构件。
治理前先确定冲突属于哪一类。只有知道根因,才知道应该升级直接依赖、调整平台、修正元数据、限制仓库,还是暂时使用解析规则。
15.10 failOnVersionConflict 的使用边界
可以在关键 Configuration 上启用冲突失败:
configurations.named("runtimeClasspath") { resolutionStrategy.failOnVersionConflict()}也可以在约定插件中对特定项目类型启用:
configurations.configureEach { if (isCanBeResolved) { resolutionStrategy.failOnVersionConflict() }}这项配置适合发现隐藏冲突,但不一定适合立即应用到所有历史项目:
- 老项目可能积累了大量可以运行但未治理的冲突。
- 不同 Configuration 可能有合理的不同版本范围。
- 测试、编译和运行时 Configuration 的冲突应分别处理。
- 开启后应配合
dependencyInsight逐项修复,而不是批量关闭检查。
推荐先在 CI 的关键任务、核心库或新模块中启用,再逐步扩大范围。
15.11 Platform、Constraint 与 force 的选择
三种方式的治理强度和语义不同:
dependencies { implementation(platform(project(":dependency-platform")))
constraints { implementation("org.example:shared-api:3.0.0") { because("统一公共 API 版本") } }}force 示例:
configurations.configureEach { resolutionStrategy.force("org.example:shared-api:3.0.0")}通常优先级和使用场景如下:
- Platform:多个模块需要成组版本对齐时使用。
- Constraint:表达某个模块的最低、严格或推荐版本时使用。
- 直接依赖升级:上游库版本过旧时优先修复直接来源。
- Resolution Rule:迁移坐标、替换实现或修复已知元数据问题时使用。
force:临时阻断高风险版本或紧急缓解问题,必须记录原因和移除计划。
force 会隐藏其他约束和兼容关系,容易让构建“通过解析”却在运行时失败。它不应该成为常规版本管理工具。
15.12 Resolution Strategy 与解析规则
可以对 Configuration 设置解析规则:
configurations.configureEach { resolutionStrategy { dependencySubstitution { substitute(module("org.example:old-client")) .using(module("org.example:new-client:2.0.0")) .because("旧坐标已迁移") } }}常见解析规则包括:
- 依赖替换:把旧模块替换为新模块或本地项目。
- 模块替换:声明两个模块不能同时存在,选择新的实现。
- 强制版本:临时指定最终版本。
- 缺失模块修复:在无法修改上游元数据时补充必要依赖。
- 版本拒绝:拒绝已知不兼容版本。
解析规则会影响依赖图,通常比普通依赖声明更难从项目结构看出。每条规则应包含:
- 适用模块和 Configuration。
- 规则的业务或安全原因。
- 预计移除版本或上游修复条件。
- 受影响项目和测试范围。
- 升级时需要重新验证的产物。
15.13 Component Selection Rule
Component Selection Rule 可以根据版本、状态或元数据拒绝候选组件:
configurations.configureEach { resolutionStrategy.componentSelection { all { if (candidate.version.contains("-rc")) { reject("生产构建不接受候选版本") } } }}规则应避免使用过于宽泛的字符串判断。更稳妥的策略是:
- 对生产和预发布构建使用不同的版本策略。
- 优先使用 Version Catalog、Constraint 或平台表达允许范围。
- 把拒绝理由写进规则,便于查看诊断输出。
- 规则升级后测试正常版本、被拒绝版本和没有可用版本的失败行为。
如果团队已经使用仓库或发布流程阻止候选版本进入生产仓库,构建脚本就不必重复维护同一套复杂规则。
15.14 排除传递依赖的诊断方法
排除依赖前先找到所有引入路径:
.\gradlew.bat dependencyInsight --dependency commons-logging --configuration runtimeClasspath局部排除:
dependencies { implementation("org.example:client:1.0.0") { exclude( group = "commons-logging", module = "commons-logging" ) }}排除后要验证:
.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat test.\gradlew.bat run需要特别注意:
- 排除一个路径不一定能从整个依赖图删除模块。
- 编译通过不代表运行时不需要被排除的类。
- 某些框架通过反射、服务加载器或配置文件使用依赖,源码中可能没有直接引用。
- 如果替代实现需要补回依赖,应使用清晰的直接声明而不是继续叠加排除规则。
15.15 依赖树、报告与构建输出
常用报告命令:
.\gradlew.bat :app:dependencies --configuration compileClasspath.\gradlew.bat :app:dependencies --configuration runtimeClasspath.\gradlew.bat :app:buildEnvironment.\gradlew.bat :app:outgoingVariants如果需要保存输出:
.\gradlew.bat :app:dependencies --configuration runtimeClasspath | Tee-Object dependency-tree.txt.\gradlew.bat :app:dependencyInsight --dependency guava --configuration runtimeClasspath | Tee-Object guava-insight.txt报告中应记录:
- Gradle Wrapper 版本和 JDK 版本。
- 执行的完整任务与 Configuration。
- 仓库、平台、约束和锁文件状态。
- 目标模块的最终版本、选择原因和变体。
- 失败时的完整异常、堆栈和最小复现命令。
不要只截取依赖树中一小段发给别人。被截断的树经常会隐藏真正的直接来源和冲突路径。
15.16 编译成功但运行失败
依赖诊断不能只停留在解析成功。常见运行时错误及排查方向:
NoClassDefFoundError 或 ClassNotFoundException
- 依赖是否声明在
runtimeOnly或其他实际运行时 Configuration。 - 分发包和
runtimeClasspath是否包含该构件。 - 是否被
exclude、仓库规则或发布配置移除。 - 是否只在 IDE 类路径中存在。
NoSuchMethodError 或 NoSuchFieldError
- 编译时和运行时使用了不同版本。
- 某个传递依赖被平台、锁文件或
force替换。 - 多个类加载器或容器提供了另一份旧版本。
- 分发目录、应用服务器和测试环境的类路径顺序不同。
ClassCastException
- 同名类型来自不同版本或不同类加载器。
- API 和实现版本没有成组对齐。
- 服务加载器加载了不兼容的实现。
建议分别检查编译、测试、运行和分发的类路径,不要只执行一次 dependencies 就得出结论。
15.17 依赖诊断与变体诊断的关系
版本选择正确,不代表变体选择正确。一个模块可能最终选择了预期版本,却选择了错误的 Usage、Category、目标 JVM 或构件类型。
排查顺序可以是:
- 用
dependencyInsight确认模块版本和选择原因。 - 用
outgoingVariants查看生产者变体。 - 查看失败信息中的 Consumer attributes 和 Producer attributes。
- 确认任务使用的是编译还是运行时 Configuration。
- 检查 JAR、classes、platform、sources 或 feature variant 是否被错误选择。
.\gradlew.bat :library:outgoingVariants.\gradlew.bat :app:dependencies --configuration runtimeClasspath.\gradlew.bat :app:dependencyInsight --dependency library --configuration runtimeClasspath不要用 force、手工复制 JAR 或指定内部 Configuration 来掩盖 No matching variant。这些做法可能绕过模型,却让发布和消费者行为更加不可预测。
15.18 锁文件与解析规则的协同
Dependency Locking 记录最终版本,但不会自动解释某条版本为什么合理。解析规则、平台和约束仍决定锁文件生成时的结果。
更新前先查看当前状态:
.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencyInsight --dependency org.example:shared-api --configuration runtimeClasspath精确更新目标模块:
.\gradlew.bat build --update-locks org.example:shared-api更新后检查:
.\gradlew.bat dependencies --write-locks.\gradlew.bat dependencyInsight --dependency org.example:shared-api --configuration runtimeClasspath.\gradlew.bat check锁文件变化异常时,重点检查:
- 是否误用了全量
--write-locks。 - 是否有动态版本或 changing module。
- 是否修改了平台、BOM 或约束。
- 是否改变了仓库顺序或元数据来源。
- 是否因为解析规则导致多个模块同时变化。
15.19 依赖验证与升级审查
版本锁定和依赖验证应配合使用:
.\gradlew.bat --write-verification-metadata sha256 check.\gradlew.bat check --dependency-verification=strict依赖升级审查至少包含:
- 坐标和版本是否来自预期仓库。
- 校验和是否发生变化,变化是否有明确原因。
- Gradle Module Metadata、POM 和实际 JAR 是否一致。
- 直接依赖和传递依赖的版本是否成组升级。
- Java、Kotlin、Android 或其他插件的最低运行要求是否变化。
- 测试、分发包和生产启动是否使用同一组运行时依赖。
如果校验失败,不要直接删除 verification-metadata.xml 中的旧记录。先确认是合法升级、仓库返回了不同构件,还是供应链异常。
15.20 依赖升级的最小变更流程
推荐将升级拆成小批次:
确认当前图 → 选择升级目标 → 修改声明或平台 → 更新锁文件 → 更新验证元数据 → 运行测试和打包 → 审查依赖树与产物具体步骤:
- 用
dependencies和dependencyInsight保存当前基线。 - 阅读上游发布说明、迁移指南和安全公告。
- 只修改 Version Catalog、平台、约束或直接依赖。
- 使用
--update-locks更新目标模块及必要的关联模块。 - 检查校验和、POM、模块元数据和构件内容。
- 运行
check、集成测试、应用分发和关键启动场景。 - 对比升级前后的运行时类路径和依赖报告。
- 记录升级原因、兼容性结论和回滚方式。
不要将“依赖树变化很大”视为一定失败,也不要将“测试通过”视为一定安全。变化需要解释,测试需要覆盖真实运行路径。
15.21 诊断脚本与可重复证据
可以在 CI 中生成基础依赖报告:
tasks.register("dependencyDiagnostics") { group = "verification" description = "输出关键 Configuration 的依赖诊断提示。" doLast { println("请执行:") println("./gradlew dependencies --configuration runtimeClasspath") println("./gradlew dependencyInsight --dependency <module> --configuration runtimeClasspath") }}更复杂的项目可以使用 Gradle Tooling API、Build Scan 或自定义插件读取解析结果,但应注意:
- 诊断逻辑不应改变正式构建的依赖选择。
- 输出中可能包含仓库地址、内部坐标和路径,不要无条件公开。
- 报告应包含 Gradle、JDK、操作系统和构建参数,便于复现。
- 自定义诊断任务要声明输入,避免每次构建都无条件触发昂贵解析。
15.22 版本冲突与依赖诊断常见问题
1. dependencyInsight 找不到模块
确认目标名称、Configuration 和项目路径。可以使用完整坐标、模块名或关键字搜索,并先运行对应 Configuration 的 dependencies。
2. 依赖树显示两个版本,但运行时只有一个
同一模块发生冲突解决后,最终类路径通常只保留选中的版本。要查看被选版本及原因,使用 dependencyInsight,不要只看树中的候选路径。
3. force 之后构建通过但运行失败
强制版本可能破坏上游库的二进制兼容性。撤销强制规则,升级直接依赖或使用官方平台,并用编译、测试和真实启动场景验证。
4. 本地与 CI 解析结果不同
比较 Wrapper、JDK、仓库顺序、代理、凭据、缓存、锁文件、验证元数据和环境变量。重点确认双方是否使用了同一个仓库元数据和相同的 Configuration。
5. 依赖升级带来大量锁文件变化
先检查是否改变了 BOM、平台、仓库或动态版本,再判断传递依赖的连锁升级是否合理。不要直接提交未经解释的全量变化。
6. 解析成功但应用分发包缺少依赖
比较 runtimeClasspath、installDist 生成的 lib 目录和启动脚本,检查应用插件、排除规则、发布配置以及运行时 Configuration 是否一致。
16. Version Catalog、BOM 与约束
这三种工具解决不同问题:
- Version Catalog:集中管理声明和别名。
- Platform/BOM:对齐一组模块版本。
- Constraint:表达允许、偏好或拒绝的版本范围。
16.1 Version Catalog
gradle/libs.versions.toml:
[versions]guava = "33.5.0-jre"junit = "6.0.3"
[libraries]guava = { module = "com.google.guava:guava", version.ref = "guava" }junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
[bundles]testing = ["junit-jupiter"]
[plugins]kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.3.20" }构建脚本:
dependencies { implementation(libs.guava) testImplementation(libs.bundles.testing)}插件别名:
plugins { alias(libs.plugins.kotlin.jvm)}Version Catalog 只是集中声明,不会自动保证所有传递依赖都使用该版本,也不会代替依赖锁定和验证。
16.2 Platform 与 BOM
dependencies { implementation(platform("org.springframework.boot:spring-boot-dependencies:4.0.3")) implementation("org.springframework:spring-core")}platform 导入版本约束,消费者仍可通过更强约束选择其他版本。
dependencies { implementation(enforcedPlatform("com.example:company-bom:1.0.0"))}enforcedPlatform 会强制版本并传递给消费者。用于 Library 时要格外谨慎,因为它可能把内部策略强加给下游。
16.3 Java Platform 项目
plugins { `java-platform`}
dependencies { constraints { api("com.google.guava:guava:33.5.0-jre") api("org.junit.jupiter:junit-jupiter:6.0.3") }}多模块项目可建立专门的 platform 模块统一版本约束。
16.4 Rich Version
dependencies { implementation("org.example:demo") { version { strictly("[2.0, 3.0[") prefer("2.5.1") reject("2.4.0") } }}Rich Version 适合表达兼容区间和已知坏版本,但规则过多会使解析难以理解。优先保持规则简单,并用 dependencyInsight 验证结果。
16.5 Version Catalog 的语义边界
Version Catalog 主要解决“如何在构建脚本中统一声明坐标、版本和别名”,不直接改变依赖解析算法:
[versions]logging = "2.0.0"
[libraries]logging-api = { module = "org.example:logging-api", version.ref = "logging" }dependencies { implementation(libs.logging.api)}这里的 libs.logging.api 最终仍然是一个普通依赖声明。以下情况都可能使最终解析版本不同于目录中写的版本:
- 传递依赖请求了更高版本。
- Platform 或 BOM 提供了版本对齐。
- Constraint 使用了
strictly、require、prefer或reject。 - Dependency Locking 固定了已解析版本。
- Resolution Rule、组件选择规则或仓库元数据改变了候选集合。
因此,修改 libs.versions.toml 后仍需使用 dependencies 和 dependencyInsight 验证最终结果。
16.6 Version Catalog 的 TOML 结构
一个完整的目录可以包含四个主要部分:
[versions]kotlin = "2.3.20"junit = "6.0.3"guava = "33.5.0-jre"
[libraries]guava = { module = "com.google.guava:guava", version.ref = "guava" }junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" }junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
[bundles]junit = ["junit-api", "junit-engine"]
[plugins]kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }四类条目的职责不同:
[versions]:保存可复用的版本值。[libraries]:保存模块坐标和版本引用。[bundles]:把多个 library 别名组合成一次声明。[plugins]:保存插件 ID 和插件版本,供alias使用。
命名建议:
- 使用小写字母、数字和连字符,避免与 Kotlin DSL 保留字冲突。
- 用
junit-api、junit-engine等名称表达组件用途。 - 不要把版本号写进别名,例如
guava-335;版本变化不应导致脚本 API 变化。 - 相关模块优先共享
[versions],但只有确认它们版本生命周期一致时才共享。 - bundle 只解决声明便利性,不表达版本对齐和运行时兼容性。
16.7 无版本别名与平台组合
当版本由 Platform 或 BOM 管理时,目录中的 library 别名可以不写版本:
[versions]junit = "6.0.3"
[libraries]junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }junit-launcher = { module = "org.junit.platform:junit-platform-launcher" }构建脚本:
dependencies { testImplementation(platform(libs.junit.bom)) testImplementation(libs.junit.jupiter) testRuntimeOnly(libs.junit.launcher)}这种组合把职责分开:
- Version Catalog 提供 BOM 和模块的类型安全别名。
- BOM 或 Platform 决定相关模块的兼容版本。
- 项目仍然显式声明实际使用的模块。
不要为了让目录中每个 library 都有版本而给平台管理的模块重复写版本,这可能制造两个版本来源。
16.8 插件别名与 apply false
根项目可以集中声明插件版本,再由子项目决定是否应用:
plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.versions) apply false}子项目:
plugins { alias(libs.plugins.kotlin.jvm)}apply false 表示解析插件但不将其应用到当前项目。它适合在根项目统一版本,避免每个子项目重复写插件版本;但插件是否应用仍应由模块职责决定。
插件别名的注意事项:
- 插件 ID 和插件版本必须与 Plugin Management、插件门户或私有插件仓库的配置兼容。
- 根项目声明插件并不意味着所有子项目都自动获得插件功能。
- 修改插件版本时要同时检查 Gradle、JDK、Kotlin 或 Android Gradle Plugin 的兼容矩阵。
- 不要把业务库版本和构建插件版本混在同一个含义不清的别名中。
16.9 在 settings.gradle.kts 中配置 Catalog
默认目录 gradle/libs.versions.toml 会自动生成名为 libs 的目录。需要自定义名称或导入其他文件时,可以在 Settings 中配置:
dependencyResolutionManagement { versionCatalogs { create("testLibs") { from(files("gradle/test-libs.versions.toml")) } }}构建脚本中使用:
dependencies { testImplementation(testLibs.junit.jupiter)}多个目录应有明确边界:
- 主目录保存大多数项目共享的库、插件和版本。
- 专项目录保存测试、发布或平台特有的依赖。
- 不要让同一个模块在多个目录中使用不同别名和不同版本来源。
- 目录名称应体现用途,避免
libs2、libsNew这类临时命名长期保留。
大多数工程优先使用一个目录。多个 Catalog 只有在组织边界或生命周期确实不同的情况下才值得引入。
16.10 在 buildSrc 和 Included Build 中使用 Catalog
buildSrc 或 build-logic 是独立的构建逻辑工程,不会自动继承主工程的 Version Catalog。需要在其 Settings 中显式导入:
dependencyResolutionManagement { versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } }}之后可以在 buildSrc 的构建脚本中使用:
dependencies { implementation(libs.gradle.kotlin.dsl)}注意事项:
buildSrc的 Catalog 路径相对于buildSrc/settings.gradle.kts解析。- Included Build 中的路径和 Catalog 导入应独立验证,不能假设与主项目目录相同。
- 构建逻辑依赖应与被构建项目的运行时依赖分开管理。
- 修改主项目 Catalog 后,应验证
buildSrc或build-logic是否仍能编译。
如果共享构建逻辑规模较大,优先使用独立 Included Build 和 Convention Plugin,避免把所有逻辑堆在 buildSrc 中。
16.11 Catalog 与 Platform 的职责分工
推荐的多项目分层如下:
gradle/libs.versions.toml # 坐标、别名、插件版本:dependency-platform # 约束和版本对齐:app # 声明实际使用的依赖:library # 声明 API 和实现依赖示例:
dependencies { implementation(platform(project(":dependency-platform"))) implementation(libs.guava) implementation(libs.slf4j.api)}职责可以这样划分:
- Catalog 解决“脚本如何引用”和“坐标如何集中维护”。
- Platform 解决“相关组件如何对齐”和“传递依赖如何受约束”。
- Constraint 解决“某个模块需要满足什么版本条件”。
- Locking 解决“本次构建最终选择了哪些版本”。
- Verification 解决“下载的构件是否与可信记录一致”。
不要用 Catalog 模拟 Platform,也不要用 Platform 代替 Locking。它们位于依赖治理链路的不同层次。
16.12 Java Platform 的完整配置
一个可发布的 Java Platform 可以这样配置:
plugins { `java-platform` `maven-publish`}
group = "com.example"version = "1.0.0"
javaPlatform { allowDependencies()}
dependencies { api(platform("org.junit:junit-bom:6.0.3"))
constraints { api(libs.guava) api("org.slf4j:slf4j-api:2.0.17") runtime("org.postgresql:postgresql:42.7.8") }}如果 Platform 需要导入其他 Platform 或 BOM,使用 allowDependencies()。Platform 本身通常不包含业务代码,主要发布约束和对齐信息。
发布 Platform 时应检查:
- POM 中是否包含预期的 dependency management 信息。
- Gradle Module Metadata 是否被发布并能表达约束和变体。
- 消费者使用
platform(project(":dependency-platform"))后是否能解析无版本模块。 - 是否错误地将内部强制版本传递给公共库消费者。
- Platform 版本升级是否有迁移说明和兼容性测试。
16.13 Platform 的严格版本策略
Gradle 对普通 Platform 和 Enforced Platform 的处理强度不同:
dependencies { implementation(platform(project(":dependency-platform")))}普通 Platform 适合共享推荐版本和兼容约束。若不希望消费者继承平台中的严格版本,可以在特定依赖声明上取消严格版本背书:
dependencies { implementation(platform(project(":dependency-platform"))) { doNotEndorseStrictVersions() }}enforcedPlatform:
dependencies { implementation(enforcedPlatform(project(":dependency-platform")))}它会把强制版本传递给消费者,公共库使用时尤其危险:
- 下游项目可能无法选择自己兼容的版本。
- 版本冲突可能从解析阶段被隐藏到运行时。
- 消费者可能需要额外排除强制平台才能恢复自主选择。
除非 Platform 明确是应用内部的统一运行时基线,否则优先使用普通 Platform、Constraint 或 rich version。
16.14 Constraint 的强度与使用场景
约束可以表达不同强度:
dependencies { constraints { implementation("org.example:api:2.0.0") { version { prefer("2.0.0") } because("默认使用经过测试的 API 版本") }
implementation("org.example:security-api") { version { strictly("[4.2, 5.0[") reject("4.5.0") } because("避免已知不兼容版本") } }}选择原则:
prefer适合作为默认建议,但允许更强的依赖关系改变结果。require表示最低或范围要求,不能接受低于要求的版本。strictly限定最终版本范围,找不到可接受版本时让解析失败。reject排除已知有问题的版本。
约束通常不会主动把一个完全未被请求的模块加入依赖图。需要确保模块存在时,应声明直接依赖;只需要控制传递模块版本时,使用 Constraint 更合适。
16.15 Rich Version 的组合规则
Rich Version 最好表达一个可解释的兼容策略:
dependencies { implementation("org.example:client") { version { strictly("[3.0, 4.0[") prefer("3.4.2") reject("3.2.0") } }}可以按强度理解:
strictly:最强,范围外版本不可接受。require:最低要求或范围要求,但更高版本仍可能在冲突解决中胜出。prefer:没有更强声明时的软偏好。reject:排除已知不应使用的版本。
不要把所有依赖都改成 strictly。过多严格范围会让普通升级变成解析失败,也可能阻止消费者使用兼容的新版本。公共库尤其要谨慎设置上限。
16.16 Catalog、Platform 与 Locking 的升级流程
三者组合时,推荐按以下顺序升级:
- 在 Catalog 中修改坐标或推荐版本。
- 检查 Platform 和 Constraint 是否仍表达正确的兼容范围。
- 使用
dependencyInsight查看目标版本是否实际被选择。 - 用
--update-locks精确更新锁文件。 - 更新依赖验证元数据并审查校验和。
- 运行单元测试、集成测试、静态检查和分发构建。
- 对比升级前后的运行时依赖图和发布元数据。
.\gradlew.bat :app:dependencyInsight --dependency guava --configuration runtimeClasspath.\gradlew.bat :app:build --update-locks com.google.guava:guava.\gradlew.bat :app:check.\gradlew.bat --write-verification-metadata sha256 check如果只修改 Catalog 版本但锁文件仍固定旧版本,最终构建可能不会使用新版本。反过来,只更新锁文件而不修改声明,也会让升级意图不清晰。两者都要审查。
16.17 版本管理综合示例
gradle/libs.versions.toml:
[versions]junit = "6.0.3"guava = "33.5.0-jre"slf4j = "2.0.17"
[libraries]junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" }guava = { module = "com.google.guava:guava", version.ref = "guava" }slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
[bundles]testing = ["junit-jupiter"]:dependency-platform/build.gradle.kts:
plugins { `java-platform`}
dependencies { constraints { api(libs.guava) api(libs.slf4j.api) } api(platform(libs.junit.bom))}:app/build.gradle.kts:
plugins { application}
dependencies { implementation(platform(project(":dependency-platform"))) implementation(libs.guava) implementation(libs.slf4j.api) testImplementation(libs.bundles.testing)}
tasks.test { useJUnitPlatform()}验证:
.\gradlew.bat :app:dependencies --configuration runtimeClasspath.\gradlew.bat :app:dependencyInsight --dependency org.slf4j:slf4j-api --configuration runtimeClasspath.\gradlew.bat :dependency-platform:outgoingVariants.\gradlew.bat check这个结构将“可读的依赖入口”和“实际参与解析的版本对齐”分开,适合多项目工程逐步演进。
16.18 版本目录与平台的常见问题
1. 修改 Catalog 后最终版本没有变化
检查是否有 Locking、Platform、Constraint 或传递依赖提供了更强的版本要求。使用 dependencyInsight 查看选择原因。
2. libs 访问器不存在
确认文件路径是 gradle/libs.versions.toml,TOML 语法正确,别名没有使用保留关键字,并重新同步或运行 Gradle 配置阶段。自定义 Catalog 名称时,应使用对应的访问器名称。
3. buildSrc 找不到 libs
buildSrc 是独立构建,需要在 buildSrc/settings.gradle.kts 中显式导入主工程 Catalog。
4. Platform 中的版本没有被采用
确认使用了 platform(...),平台和实际模块属于同一 Configuration,并检查是否存在更强的 Constraint、force、锁文件或 enforcedPlatform。
5. 使用 enforcedPlatform 后下游项目无法升级
检查公共库是否把强制平台传递给了消费者。通常应改为普通 Platform、Constraint 或适当的 rich version。
6. Bundle 引入了不需要的模块
Bundle 只是依赖集合。拆分 bundle、使用更细粒度别名,或直接声明实际需要的模块。
7. strictly 导致解析失败
检查依赖图中是否没有满足范围的版本,或者上游 Platform 要求了范围外版本。扩大范围前要先确认 API 和运行时兼容性。
17. 仓库管理
17.1 常见仓库
repositories { mavenCentral() google() mavenLocal()}普通 JVM 项目通常不需要 google();非本地开发场景一般不建议依赖 mavenLocal(),因为它会引入机器状态差异。
17.2 在 Settings 中统一仓库
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() }}模式含义:
PREFER_PROJECT:优先项目仓库。PREFER_SETTINGS:优先 Settings 仓库。FAIL_ON_PROJECT_REPOS:项目脚本声明仓库时失败。
统一仓库可以降低子项目顺序差异和供应链风险。
17.3 私有 Maven 仓库
repositories { maven { name = "companyPackages" url = uri("https://repo.example.com/releases") credentials { username = providers.gradleProperty("repoUser").orNull password = providers.gradleProperty("repoPassword").orNull } }}凭据应来自用户级属性、环境变量或 CI Secret,不应写死在仓库代码中。
17.4 仓库内容过滤
repositories { maven { url = uri("https://repo.example.com/releases") content { includeGroupByRegex("com\\.example(\\..*)?") } } mavenCentral()}内容过滤可以减少错误仓库命中、网络请求和依赖混淆风险。
17.5 仓库顺序
Gradle 会按仓库声明顺序查找模块元数据。不要在不同仓库发布相同坐标但内容不同的构件,否则构建结果可能受仓库顺序影响。
17.6 仓库类型与适用场景
Gradle 常用仓库类型及适用场景如下:
| 仓库类型 | 适用场景 | 主要注意事项 |
|---|---|---|
| Maven | 公共库、企业内部库和大多数 JVM 项目 | 支持 POM、JAR、Sources 和模块元数据 |
| Ivy | Ivy 生态或历史构建系统 | 需要确认组织和模块命名规则 |
| Flat Directory | 临时本地 JAR 或遗留项目 | 缺少可靠的传递依赖和版本元数据 |
mavenLocal() | 验证本地发布结果 | 会引入机器状态,不适合作为默认生产仓库 |
| Google Maven | Android 和 Google 生态构件 | 普通 JVM 项目通常不需要 |
| Gradle Plugin Portal | 通过插件 ID 解析公开 Gradle 插件 | 与普通项目依赖仓库职责不同 |
flatDir 示例:
repositories { flatDir { dirs("libs") }}
dependencies { implementation(name = "legacy-sdk", ext = "jar")}只要能发布到 Maven 或 Ivy 仓库,就不应长期依赖 libs 目录中的裸 JAR。
17.7 项目依赖仓库与插件仓库
Gradle 至少存在两类常见仓库配置:
pluginManagement { repositories { gradlePluginPortal() mavenCentral() }}
dependencyResolutionManagement { repositories { mavenCentral() }}pluginManagement.repositories:解析plugins {}中的插件请求。dependencyResolutionManagement.repositories:解析项目依赖、平台和工具依赖。buildscript.repositories:传统buildscript脚本类路径依赖的仓库。- 项目
repositories {}:项目 Configuration 使用的普通依赖仓库。
插件能够在应用时向项目添加仓库,因此企业构建应审查插件行为,并通过 Settings 中的仓库模式限制不受控的仓库声明。
17.8 Settings 仓库模式
推荐在 settings.gradle.kts 中统一声明依赖仓库:
import org.gradle.api.initialization.resolve.RepositoriesMode
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() }}三种模式的含义:
PREFER_PROJECT:优先使用项目仓库,适合过渡阶段。PREFER_SETTINGS:优先使用 Settings 仓库,项目仓库声明通常会收到警告。FAIL_ON_PROJECT_REPOS:项目或插件添加仓库时让构建失败,适合统一治理。
第三方插件或约定插件动态添加仓库时,也可能触发 FAIL_ON_PROJECT_REPOS。启用前应清点构建逻辑和插件行为。
17.9 仓库内容过滤
内容过滤可以减少网络请求、限制依赖来源并降低坐标混淆风险:
dependencyResolutionManagement { repositories { maven { name = "companyRepository" url = uri("https://repo.example.com/maven/releases") content { includeGroupByRegex("com\\.example(\\..*)?") includeGroup("org.example.internal") } } mavenCentral { content { excludeGroupByRegex("com\\.example(\\..*)?") } } }}过滤规则应覆盖所有可能提供该坐标的仓库,不能只限制私有仓库而让公共仓库继续提供同一组织的模块。
17.10 Exclusive Content
当某个组织的模块必须来自指定仓库时,使用独占内容:
dependencyResolutionManagement { repositories { exclusiveContent { forRepository { maven { name = "companyRepository" url = uri("https://repo.example.com/maven/releases") } } filter { includeGroupByRegex("com\\.example(\\..*)?") } } mavenCentral() }}独占内容比普通 content 过滤更严格:匹配的模块不会从其他仓库寻找。必须同时确认该模块的传递依赖能够从允许的其他仓库解析。
17.11 私有仓库认证与凭据
凭据应来自 Provider、用户级属性或 CI Secret:
repositories { maven { name = "companyPackages" url = uri("https://repo.example.com/maven/releases") credentials { username = providers.gradleProperty("repoUser").orNull ?: providers.environmentVariable("REPO_USER").orNull password = providers.gradleProperty("repoPassword").orNull ?: providers.environmentVariable("REPO_PASSWORD").orNull } }}推荐的凭据来源优先级:
- CI Secret 或受保护环境变量。
- 用户级
~/.gradle/gradle.properties。 - 本地临时系统属性或命令行参数。
- 项目级
gradle.properties,仅保存非敏感配置。
禁止把密码、Token 或私钥写入构建脚本、版本控制、构建日志和公开的扫描结果。凭据 Provider 应保持惰性,避免在配置阶段读取或打印 Secret。
17.12 gradle.properties 与 CI 环境
用户级属性示例:
repoUser=developerrepoPassword=local-tokenCI 可以通过环境变量注入项目属性:
ORG_GRADLE_PROJECT_repoUserORG_GRADLE_PROJECT_repoPassword构建脚本统一使用:
val repoUser = providers.gradleProperty("repoUser")val repoPassword = providers.gradleProperty("repoPassword")不要把 System.getenv() 和 System.getProperty() 的读取散落在多个脚本中。统一使用 Provider 能减少配置阶段副作用,并让凭据缺失时的错误更容易解释。
17.13 Release、Snapshot 与仓库内容
可以把 release 与 snapshot 仓库分开:
repositories { maven { name = "releases" url = uri("https://repo.example.com/maven/releases") mavenContent { releasesOnly() } } maven { name = "snapshots" url = uri("https://repo.example.com/maven/snapshots") mavenContent { snapshotsOnly() } }}Snapshot 或 changing module 可能在相同版本字符串下对应不同构件,不适合作为生产构建的长期输入。生产构建应优先使用不可变 release,并配合 Dependency Locking 和 Dependency Verification。
17.14 mavenLocal() 的使用边界
mavenLocal() 适合验证本地发布结果:
.\gradlew.bat publishToMavenLocal临时消费:
repositories { mavenLocal() mavenCentral()}不建议默认加入 mavenLocal(),因为它可能导致本机有构件而 CI 没有、旧版本覆盖远程构件、依赖图无法复现。需要验证本地模块时,优先考虑 Composite Build、临时测试仓库或明确的本地开发配置。
17.15 仓库顺序与相同坐标
多个仓库提供相同坐标但内容不同,会让构建结果受仓库顺序影响:
companyRepository: com.example:shared:1.0.0 -> 内部构件mavenCentral: com.example:shared:1.0.0 -> 另一份构件治理方式:
- 为内部组织配置 exclusive content。
- 不在公共仓库复用内部组织坐标。
- 禁止覆盖已经发布的 release 版本。
- 对 POM、Gradle Module Metadata 和构件启用验证。
- 变更仓库顺序时生成依赖报告并审查差异。
如果同一组件需要不同能力,应使用规范的变体元数据,而不是依赖仓库顺序选择不同文件。
17.16 元数据来源
Gradle 可能从不同格式理解同一个模块:
- Maven POM:兼容范围广,但表达能力相对传统。
- Ivy 描述:适合 Ivy 生态和历史仓库。
- Gradle Module Metadata:能够表达变体、Capabilities、约束和更丰富的依赖关系。
可以配置仓库的元数据来源:
repositories { maven { url = uri("https://repo.example.com/maven/releases") metadataSources { gradleMetadata() mavenPom() artifact() } }}metadataSources 应谨慎使用:关闭某种元数据可能导致变体、约束或传递依赖丢失;只使用 artifact() 时,Gradle 无法获得完整模块依赖关系。修改后应重新运行依赖树、变体报告和运行时测试。
17.17 插件仓库与插件解析
pluginManagement 控制插件解析:
pluginManagement { repositories { maven { name = "companyPlugins" url = uri("https://repo.example.com/gradle-plugins") content { includeGroupByRegex("com\\.example(\\..*)?") } } gradlePluginPortal() }}私有插件如果没有正确发布 Plugin Marker,可以映射到实现模块:
pluginManagement { resolutionStrategy { eachPlugin { if (requested.id.id == "com.example.conventions") { useModule("com.example:conventions-plugin:${requested.version}") } } }}插件仓库和普通项目依赖仓库不互相继承。应分别检查插件 ID、Plugin Marker、实现模块、插件传递依赖和仓库来源。
17.18 缓存、离线模式与刷新
常用命令:
.\gradlew.bat build --offline.\gradlew.bat build --refresh-dependencies.\gradlew.bat build --info--offline只使用本地缓存,适合验证缓存完整性,缺少依赖时会失败。--refresh-dependencies重新检查依赖状态,适合处理元数据变化,不应作为日常默认参数。--info可以查看仓库请求和缓存命中,但日志可能包含内部坐标和 URL。
缓存不是供应链完整性证明,即使构件来自本地缓存,也应配合 Dependency Verification。
17.19 仓库与依赖验证
验证元数据通常位于:
gradle/verification-metadata.xml生成和严格验证:
.\gradlew.bat --write-verification-metadata sha256 check.\gradlew.bat check --dependency-verification=strict仓库治理和依赖验证的职责不同:
- 仓库过滤控制“允许从哪里找”。
- Locking 控制“允许选择哪些结果”。
- Verification 控制“下载内容是否与可信记录一致”。
- 发布权限和不可变策略控制“谁可以提供构件”。
更换仓库或镜像后,应审查元数据、构件和校验和,不要直接删除旧验证记录。
17.20 企业仓库治理建议
大型团队可以通过 Settings 插件或 Convention Plugin 统一仓库策略:
import org.gradle.api.initialization.resolve.RepositoriesMode
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { exclusiveContent { forRepository { maven { name = "companyReleases" url = uri("https://repo.example.com/maven/releases") } } filter { includeGroupByRegex("com\\.example(\\..*)?") } } mavenCentral { content { excludeGroupByRegex("com\\.example(\\..*)?") } } }}企业策略至少应包含:
- 允许的仓库列表和用途。
- 组织坐标与仓库的独占映射。
- release、snapshot、第三方代理和插件仓库边界。
- 凭据最小权限、审计日志和轮换流程。
- 构件不可变、校验和、签名和漏洞扫描策略。
- 仓库不可用时的镜像、缓存和恢复策略。
17.21 仓库问题排错流程
1. Could not resolve:确认坐标、URL、内容过滤、认证、代理、证书和仓库状态,并使用 --info 查看实际请求。
2. 本地成功、CI 失败:检查 mavenLocal()、用户级缓存、凭据、代理、镜像、锁文件和验证元数据,优先在干净的 GRADLE_USER_HOME 中复现。
3. 解析到了错误仓库:检查仓库顺序和过滤规则,确认同一坐标没有在多个仓库中重复发布,必要时使用 exclusive content。
4. 插件能解析、普通依赖不能解析:分别检查 pluginManagement.repositories 和 dependencyResolutionManagement.repositories。
5. 认证失败:确认 Provider 读取到凭据、环境变量名称正确、CI Secret 已注入,并避免在日志中输出敏感信息。
6. 元数据导致变体异常:比较 POM、Gradle Module Metadata 和 metadataSources 配置,不要直接修改缓存文件。
17.22 仓库管理综合示例
下面的 Settings 配置展示插件仓库、项目仓库、独占内容和内容过滤:
import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement { repositories { maven { name = "companyPlugins" url = uri("https://repo.example.com/gradle-plugins") content { includeGroupByRegex("com\\.example(\\..*)?") } } gradlePluginPortal() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { exclusiveContent { forRepository { maven { name = "companyReleases" url = uri("https://repo.example.com/maven/releases") } } filter { includeGroupByRegex("com\\.example(\\..*)?") } } mavenCentral { content { excludeGroupByRegex("com\\.example(\\..*)?") } } }}验证顺序:
.\gradlew.bat buildEnvironment.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencyInsight --dependency com.example --configuration runtimeClasspath.\gradlew.bat build --info.\gradlew.bat --write-verification-metadata sha256 check18. 多项目构建
18.1 典型结构
demo/├── settings.gradle.kts├── build.gradle.kts├── app/│ └── build.gradle.kts├── core/│ └── build.gradle.kts└── service/ └── build.gradle.kts18.2 声明子项目
rootProject.name = "demo"include("app", "core", "service")18.3 模块依赖
app/build.gradle.kts:
dependencies { implementation(project(":service"))}service/build.gradle.kts:
dependencies { implementation(project(":core"))}推荐依赖方向:
app → service → core避免循环依赖。模块拆分要基于稳定职责和依赖边界,而不是为了让目录看起来“架构化”。
18.4 执行子项目任务
.\gradlew.bat :app:build.\gradlew.bat :core:test.\gradlew.bat :service:dependencies --configuration runtimeClasspath18.5 查看项目结构
.\gradlew.bat projects.\gradlew.bat tasks --all18.6 根项目职责
根项目可以:
- 声明不直接应用的插件版本。
- 提供聚合任务。
- 承载版本平台或发布编排。
- 保持尽量少的跨项目动态配置。
不要在根 build.gradle.kts 中堆积大量 allprojects {} 和 subprojects {}。这会产生隐式耦合、配置性能问题和难以追踪的行为。
18.7 Composite Build
Settings 中引入另一个独立构建:
includeBuild("build-logic")Composite Build 适合:
- 开发独立插件。
- 复用构建逻辑。
- 在源码状态下替换外部模块。
- 组合仍需独立发布和版本化的构建。
它和普通子项目不同:每个 Included Build 有自己的 Settings、项目空间和构建边界。
18.8 Project Path 与目录映射
项目路径由 settings.gradle.kts 定义,默认与目录结构一致:
rootProject.name = "demo"include(":app", ":core", ":feature:login")默认目录:
app/core/feature/login/如果实际目录需要与逻辑项目路径分离,可以显式映射:
include(":app", ":core", ":feature:login")
project(":feature:login").projectDir = file("features/login")项目路径的设计原则:
- 路径表达稳定的模块职责,不要频繁随物理目录调整。
- 逻辑路径尽量短且唯一,避免多个模块使用相似名称。
settings.gradle.kts负责项目边界,子项目脚本负责模块配置。- 目录迁移时保持项目路径不变,可以减少依赖声明和 CI 任务变化。
18.9 多项目的依赖方向
推荐让依赖图从外层应用指向内层能力:
:app ───────▶ :service ───────▶ :core │ ▲ └──────────────▶ :api ─────────┘项目依赖:
dependencies { implementation(project(":service"))}依赖方向应满足:
- 底层模块不依赖上层应用模块。
- 公共 API 和实现可以拆成不同项目。
- 测试工具、代码生成工具和运行时模块不要相互形成环。
- 依赖图中的每条边都能说明稳定的职责关系。
Gradle 可以发现部分循环依赖,但“能够配置”不代表模块边界合理。循环通常意味着职责、接口或发布边界需要重新设计。
18.10 api、implementation 与项目依赖
项目依赖同样遵循 Java Library 的 API 边界:
dependencies { api(project(":api")) implementation(project(":core"))}api(project(":api")):service的公共方法签名暴露api类型时使用。implementation(project(":core")):core只在service内部使用时使用。
消费者的编译类路径不应因为上游的实现依赖而自动暴露所有内部模块。多项目拆分的价值之一就是让 API 边界可以被依赖解析和编译器验证。
18.11 多项目中的变体选择
implementation(project(":core")) 不是简单地读取 core/build/libs/core.jar。Gradle 会根据消费者 Configuration 请求生产者的匹配变体:
.\gradlew.bat :core:outgoingVariants.\gradlew.bat :app:dependencies --configuration runtimeClasspath常见请求关系:
compileClasspath请求生产者的 API 编译能力。runtimeClasspath请求生产者的运行时能力。testRuntimeClasspath可能需要测试相关依赖或测试夹具。- 平台消费者请求 Platform 变体,而不是普通库变体。
出现 No matching variant 时,应同时检查消费者 Configuration 和生产者 outgoingVariants,不要直接指定内部 Configuration 或复制构件。
18.12 跨项目共享构件
项目可以通过可消费 Configuration 发布 Schema、报告或其他构件,消费者使用项目依赖获取:
val schemaElements by configurations.creating { isCanBeConsumed = true isCanBeResolved = false}
val generateSchema by tasks.registering { val output = layout.buildDirectory.file("schema/model.json") outputs.file(output) doLast { output.get().asFile.apply { parentFile.mkdirs() writeText("{\"version\":\"1\"}") } }}
artifacts { add(schemaElements.name, generateSchema)}消费者应通过项目依赖和属性匹配获取构件,而不是读取另一个项目的 build/ 目录:
dependencies { implementation(project(":schema"))}直接访问其他项目的内部文件会绕过变体、任务输入输出和构建缓存模型,容易产生执行顺序与增量构建问题。
18.13 根项目的合理职责
根项目可以负责:
- 声明不直接应用的插件版本。
- 配置 Settings 级别的仓库和插件管理。
- 提供
checkAll、buildAll等聚合任务。 - 引入 Platform、Convention Plugin 或 build logic。
- 编排发布、签名、验证和 CI 入口。
根项目不应成为所有子项目逻辑的集中垃圾桶。尤其要谨慎使用:
allprojects { // 不推荐:对所有项目无条件注入复杂逻辑}
subprojects { // 不推荐:通过项目名称和条件堆叠模块差异}优先使用插件和约定表达共享配置,使每个项目的构建脚本可以独立理解和测试。
18.14 Convention Plugin
Convention Plugin 将共享约定封装为可复用插件,例如统一 Java Toolchain、测试和代码质量检查:
plugins { `java-library` checkstyle}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8"}
tasks.test { useJUnitPlatform()}子项目只需要:
plugins { id("java-library-conventions")}Convention Plugin 适合表达组织级约定,不应把每个模块的业务依赖和特殊行为都隐藏进去。插件名称、输入、默认值和覆盖方式应有文档与测试。
18.15 buildSrc 与 build-logic
buildSrc 可以快速放置共享构建逻辑,但它是一个隐式 Included Build,变更可能影响整个构建的配置阶段。规模较大时,推荐使用显式的 build-logic Included Build:
build-logic/├── settings.gradle.kts├── build.gradle.kts└── src/main/kotlin/ └── java-library-conventions.gradle.kts根 Settings:
pluginManagement { includeBuild("build-logic")}build-logic/settings.gradle.kts:
rootProject.name = "build-logic"两种方式的选择:
buildSrc:适合少量、快速验证的共享逻辑。build-logic:适合需要独立测试、清晰依赖和明确边界的构建逻辑。- 独立插件工程:适合需要单独发布、版本化和被多个仓库消费的插件。
构建逻辑的依赖应与生产代码依赖分开管理,并在独立工程中使用自己的 Version Catalog 或 Convention Plugin。
18.16 根项目聚合任务
可以使用聚合任务统一执行多个子项目任务:
tasks.register("buildAll") { group = "verification" description = "构建所有可构建项目。" dependsOn(subprojects.map { it.tasks.named("build") })}更推荐使用项目依赖和标准生命周期任务,让 Gradle 根据任务图决定需要执行的模块:
tasks.named("check") { dependsOn(subprojects.map { it.tasks.named("check") })}聚合任务应避免:
- 通过
afterEvaluate动态查找任务。 - 直接调用另一个项目的任务动作。
- 为了“保险”无条件依赖所有项目的所有任务。
- 在配置阶段解析所有子项目的依赖文件。
18.17 跨项目配置的常见陷阱
以下写法会制造隐式耦合:
// 不推荐project(":core") { tasks.named("jar") { dependsOn(":app:generateSources") }}问题包括:
- 底层项目知道上层应用的任务名称。
- 任务关系与依赖方向相反,难以迁移或复用。
- 配置阶段必须访问其他项目的任务模型。
- 任务重命名、项目重构或配置缓存可能受到影响。
更好的设计是让生成源码所属项目声明输入,让消费者通过项目依赖或变体获取生成结果。跨项目任务依赖只有在明确的发布编排或聚合入口中才应出现。
18.18 Composite Build 的依赖替换
Composite Build 可以把独立构建中的发布模块替换为源码项目:
includeBuild("../shared-library")如果独立构建的 group 和项目名与外部模块坐标匹配,Gradle 可以进行默认依赖替换:
dependencies { implementation("com.example:shared-library:1.2.0")}也可以显式配置替换:
includeBuild("../shared-library") { dependencySubstitution { substitute(module("com.example:shared-library")) .using(project(":")) }}Composite Build 适合本地开发独立发布的库、插件和构建逻辑,但它与普通子项目不同:被包含构建有自己的 Settings、项目空间、版本和构建生命周期。
18.19 Composite Build 的边界与排错
源码替换并不总是等价于从远程仓库消费已发布构件。可能存在:
- 发布元数据与默认项目配置不一致。
- 独立构建没有正确声明
group或项目名称。 - 生产构建的 POM、Gradle Module Metadata 和源码变体不同。
- 本地替换成功,但发布后的消费者失败。
排查命令:
.\gradlew.bat projects.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencyInsight --dependency shared-library --configuration runtimeClasspath如果需要验证远程构件而不是源码替换,可以关闭全局 Composite Build 替换规则:
configurations.configureEach { resolutionStrategy.useGlobalDependencySubstitutionRules = false}此配置适合发布验证或比较源码构建与仓库构件,不应无条件写入所有项目。
18.20 多项目构建的性能
多项目性能主要受到配置阶段、任务图规模、依赖解析和跨项目耦合影响。建议:
- 使用
tasks.register、named和configureEach延迟配置。 - 避免在根脚本中遍历所有项目并访问大量任务或文件。
- 将共享逻辑放进 Convention Plugin,减少重复配置。
- 让任务输入输出完整声明,启用增量构建和 Build Cache。
- 只让必要的项目参与任务图。
- 使用
--profile、Build Scan 或--info定位配置和执行耗时。
.\gradlew.bat :app:build --scan.\gradlew.bat :app:build --profile多项目拆分本身不会自动提升性能。如果根项目仍在配置阶段强制访问每个子项目的全部模型,模块数量增加反而会放大配置成本。
18.21 Configuration Cache 与项目隔离
Configuration Cache 会缓存一次构建的配置结果;多项目构建需要避免在配置阶段访问不安全的全局状态、项目对象或外部环境。
验证命令:
.\gradlew.bat :app:build --configuration-cache项目隔离能力对多项目构建有更严格的边界要求。启用或评估相关特性时,应先检查:
- 任务是否在执行阶段读取项目模型。
- 一个项目的插件是否直接访问另一个项目对象。
- 根脚本是否依赖所有子项目的配置副作用。
- 自定义插件是否通过全局单例共享可变状态。
不要为了追求配置缓存或项目隔离而一次性重写全部构建逻辑。先从共享约定插件、纯任务输入输出和明确项目依赖开始。
18.22 多项目测试与发布
多项目工程应分层验证:
.\gradlew.bat :core:test.\gradlew.bat :service:test.\gradlew.bat :app:test.\gradlew.bat check.\gradlew.bat publish发布库模块时检查:
api与implementation是否符合公共 API。- 项目依赖是否正确出现在 POM 和 Gradle Module Metadata。
- Sources、Javadoc、签名和校验元数据是否完整。
- 消费者从仓库解析时是否与多项目源码构建一致。
- 应用模块是否只发布应该发布的分发包和运行时依赖。
不要只执行根项目的 build 就认为所有项目的发布契约正确;应为关键库模块建立独立的发布消费测试。
18.23 多项目问题排错
1. 子项目没有被执行
确认 settings.gradle.kts 中使用了 include,项目路径和目录映射正确,并运行 gradlew projects 查看实际结构。
2. 项目依赖解析不到变体
运行生产者的 outgoingVariants,再检查消费者 Configuration、Usage、Category、目标 JVM 和 Library Elements。
3. 根项目配置影响了不相关模块
搜索 allprojects、subprojects、afterEvaluate 和按项目名称分支,优先迁移到 Convention Plugin。
4. Composite Build 没有替换外部依赖
检查 Included Build 的 group、项目名称、依赖坐标和替换规则,使用 dependencyInsight 查看最终选择。
5. 源码构建成功但发布消费失败
从临时仓库消费已发布构件,比较 POM、Gradle Module Metadata、变体、运行时类路径和实际分发包。
6. 多项目配置很慢
使用 Build Scan 或 --profile 区分配置阶段和任务执行耗时,减少根脚本遍历、跨项目访问和过早依赖解析。
18.24 多项目综合结构
推荐的工程结构:
demo/├── settings.gradle.kts├── build.gradle.kts├── gradle/libs.versions.toml├── build-logic/│ └── src/main/kotlin/java-library-conventions.gradle.kts├── app/│ └── build.gradle.kts├── api/│ └── build.gradle.kts├── service/│ └── build.gradle.kts├── core/│ └── build.gradle.kts└── dependency-platform/ └── build.gradle.kts依赖方向:
app → service → api └──→ core所有模块 → dependency-platform根 Settings 负责项目边界、仓库和 build logic;Convention Plugin 负责共享构建约定;Platform 负责版本对齐;各子项目只声明自己的 API、实现和测试依赖。
19. 复用构建逻辑
19.1 不推荐复制脚本
把相同的插件、Java 版本、测试配置和质量规则复制到几十个模块,会造成升级遗漏和行为漂移。
19.2 buildSrc
buildSrc 是约定目录,Gradle 会自动编译并加入主构建脚本类路径:
buildSrc/├── build.gradle.kts└── src/main/kotlin/ └── my-java-conventions.gradle.kts优点:简单、自动发现。局限:它与主构建耦合较强,修改可能影响整个构建的配置缓存和重新编译范围。
19.3 Included Build 方式
build-logic/├── settings.gradle.kts├── build.gradle.kts└── src/main/kotlin/ └── company.java-library.gradle.kts根 Settings:
pluginManagement { includeBuild("build-logic")}模块应用:
plugins { id("company.java-library")}对于中大型项目,Included Build 中的 Convention Plugin 通常比根脚本跨项目配置更清晰。
19.4 Convention Plugin 的职责
适合统一:
- Toolchain。
- 编译器参数。
- 测试框架。
- 代码质量插件。
- 发布约定。
- 通用依赖约束。
不适合隐藏所有模块差异。模块特有依赖和业务配置仍应留在模块脚本中。
19.5 预编译脚本插件
预编译脚本插件位于 buildSrc 或 Included Build 的插件源集,文件名决定插件 ID:
build-logic/├── build.gradle.kts└── src/main/kotlin/ └── company.java-library-conventions.gradle.ktsbuild-logic/build.gradle.kts:
plugins { `kotlin-dsl`}根 Settings:
pluginManagement { includeBuild("build-logic")}子项目:
plugins { id("company.java-library-conventions")}预编译脚本插件由 Gradle 编译为真正的插件,具有类型安全 DSL、IDE 支持和可复用性。文件名、包名和插件 ID 应保持稳定,避免重命名导致所有消费者脚本同时修改。
19.6 插件 ID 与目录组织
不带包名的文件:
src/main/kotlin/java-library-conventions.gradle.kts应用:
plugins { id("java-library-conventions")}带包名的文件:
src/main/kotlin/company/java-library-conventions.gradle.kts脚本顶部:
package company应用:
plugins { id("company.java-library-conventions")}组织插件时建议按职责拆分:
build-logic/src/main/kotlin/├── company.java-base.gradle.kts├── company.java-library.gradle.kts├── company.kotlin-library.gradle.kts├── company.publishing.gradle.kts└── company.quality.gradle.kts不要创建一个应用到所有模块的“万能插件”。插件边界应与模块类型、发布能力或质量标准对应。
19.7 Convention Plugin 的默认值与覆盖
Convention Plugin 应提供默认约定,但保留必要的项目级配置入口:
plugins { `java-library`}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8"}子项目可以在插件之后覆盖明确的配置:
plugins { id("company.java-library-conventions")}
java { toolchain { languageVersion = JavaLanguageVersion.of(17) }}约定的默认值要有文档。隐式覆盖、根据项目名称分支和通过全局变量改变行为,会让插件难以测试和升级。
19.8 插件 Extension 与声明式 DSL
需要让消费者配置插件时,应创建 Extension,而不是让消费者修改插件内部变量:
abstract class CompanyQualityExtension { abstract val strict: Property<Boolean> abstract val excludedPackages: ListProperty<String>}插件注册 Extension:
abstract class CompanyQualityPlugin : Plugin<Project> { override fun apply(project: Project) { val extension = project.extensions.create<CompanyQualityExtension>("companyQuality") extension.strict.convention(true) extension.excludedPackages.convention(emptyList()) }}消费者配置:
companyQuality { strict = true excludedPackages.add("com.example.generated")}Extension 设计建议:
- 使用
Property<T>、ListProperty<T>、SetProperty<T>和DirectoryProperty等 Provider 类型。 - 为属性提供合理的
convention默认值。 - 尽量避免在配置阶段读取
get()。 - 将用户配置映射到任务属性,而不是在插件应用时直接执行工作。
- 公开稳定、少量且有文档的配置项。
19.9 Plugin 应用顺序与 withPlugin
插件之间可能存在应用顺序关系。不要假设另一个插件已经应用,应使用 pluginManager.withPlugin:
class CompanyPublishingPlugin : Plugin<Project> { override fun apply(project: Project) { project.pluginManager.withPlugin("maven-publish") { project.extensions.configure<PublishingExtension> { // 配置发布约定 } } }}如果插件必须依赖另一个插件,可以明确应用:
class CompanyJavaLibraryPlugin : Plugin<Project> { override fun apply(project: Project) { project.pluginManager.apply("java-library") project.pluginManager.apply("maven-publish") }}不要通过 afterEvaluate 等待插件应用。withPlugin 更清晰,也更适合配置缓存、并行配置和插件组合。
19.10 懒配置 Task 与 Extension
插件应使用注册和懒配置 API:
val generateReport = tasks.register<GenerateReportTask>("generateReport") { reportFile.set(layout.buildDirectory.file("reports/company/report.json"))}
tasks.named("check") { dependsOn(generateReport)}避免:
// 不推荐:立即创建并配置任务val generateReport = tasks.create("generateReport")插件代码还应避免在应用阶段执行网络请求、扫描整个文件系统、解析所有 Configuration 或直接读取环境变量。任务真正需要这些输入时,再通过 Provider 和任务属性传入。
19.11 自定义 Task 类型
自定义 Task 应声明输入、输出和动作:
abstract class GenerateReportTask : DefaultTask() { @get:Input abstract val projectVersion: Property<String>
@get:OutputFile abstract val reportFile: RegularFileProperty
@TaskAction fun generate() { val output = reportFile.get().asFile output.parentFile.mkdirs() output.writeText("version=${projectVersion.get()}\n") }}注册任务:
tasks.register<GenerateReportTask>("generateReport") { projectVersion.set(project.providers.provider { project.version.toString() }) reportFile.set(layout.buildDirectory.file("reports/company/report.properties"))}任务设计原则:
- 所有影响结果的值都声明为输入。
- 所有生成文件都声明为输出。
@TaskAction只执行工作,不负责决定任务配置。- 使用
RegularFileProperty、DirectoryProperty等类型表达路径。 - 不在任务动作中修改未声明的外部文件或全局状态。
19.12 插件中的依赖与 Configuration
插件可以创建专用 Configuration,但应保持声明和解析角色分离:
val formatterDependencies = configurations.dependencyScope("formatterDependencies")val formatterClasspath = configurations.resolvable("formatterClasspath") { extendsFrom(formatterDependencies)}
dependencies { add(formatterDependencies.name, "org.example:formatter:1.0.0")}任务使用类路径:
tasks.register<JavaExec>("formatSources") { classpath(formatterClasspath) mainClass = "org.example.Formatter"}不要把插件实现依赖、被配置项目的生产依赖和任务工具依赖混在一起。插件的实现类路径只应包含插件本身需要的 API;任务工具应通过专用 Configuration 提供。
19.13 在构建逻辑中使用外部插件
预编译脚本插件若需要应用外部插件,通常要在 build-logic 中声明该插件实现依赖:
plugins { `kotlin-dsl`}
dependencies { implementation("org.example:quality-gradle-plugin:1.0.0")}预编译插件:
plugins { id("org.example.quality")}外部插件版本应在构建逻辑中集中管理,并检查 Gradle、JDK 和插件的兼容范围。不要让每个子项目通过不同版本重复应用同一个构建插件。
19.14 buildSrc 与 build-logic 的选择
| 方案 | 适合场景 | 主要特点 |
|---|---|---|
| 普通脚本插件 | 临时、局部逻辑 | 易开始,复用和测试能力较弱 |
buildSrc | 小型单仓库共享逻辑 | 自动发现,但与主构建耦合 |
build-logic Included Build | 中大型多项目工程 | 边界清晰、可独立测试和演进 |
| 独立二进制插件 | 多仓库或组织级复用 | 可发布、版本化和独立兼容治理 |
迁移路径可以是:
build.gradle.kts 中的重复逻辑 ↓预编译脚本插件 ↓build-logic Convention Plugin ↓独立二进制插件不要为了“看起来高级”立即创建独立插件。先确认逻辑确实跨项目或跨仓库复用,再决定发布边界。
19.15 二进制 Gradle Plugin
需要公开发布、复杂测试或跨仓库复用时,可以使用 java-gradle-plugin:
plugins { `java-gradle-plugin` `kotlin-dsl` `maven-publish`}
gradlePlugin { plugins { create("companyQuality") { id = "com.example.quality" implementationClass = "com.example.QualityPlugin" } }}插件实现:
class QualityPlugin : Plugin<Project> { override fun apply(project: Project) { project.tasks.register<GenerateReportTask>("generateQualityReport") }}二进制插件相比脚本插件更适合:
- 明确的 API 和 Extension。
- 独立单元测试与功能测试。
- 发布到 Maven、Plugin Portal 或企业插件仓库。
- 多仓库、多团队和多版本兼容。
实现语言优先选择 Java 或 Kotlin 等静态类型语言,减少运行时类型错误和二进制兼容风险。
19.16 Plugin API 与实现依赖
插件项目可以使用 Gradle 提供的特殊依赖:
dependencies { implementation(gradleApi()) testImplementation(gradleTestKit())}插件开发要区分:
- Gradle 公共 API:插件实现需要的
Project、Task、Provider 和属性类型。 - 测试框架:JUnit、TestNG 或其他测试引擎。
- TestKit:通过真实 Gradle 执行功能测试。
- 外部插件 API:插件需要配置或扩展的第三方插件。
不要依赖 Gradle 内部实现包或未公开的类。内部 API 可能在 Gradle 升级时改变,导致插件只能在特定版本工作。
19.17 Plugin TestKit 功能测试
TestKit 通过真实 Gradle 构建验证插件行为:
dependencies { testImplementation(gradleTestKit()) testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")}
tasks.test { useJUnitPlatform()}测试示例:
class QualityPluginFunctionalTest { @TempDir lateinit var projectDir: Path
@Test fun `plugin creates report task`() { projectDir.resolve("settings.gradle.kts").writeText("rootProject.name = \"sample\"\n") projectDir.resolve("build.gradle.kts").writeText( "plugins { id(\"com.example.quality\") }\n" )
val result = GradleRunner.create() .withProjectDir(projectDir.toFile()) .withPluginClasspath() .withArguments("tasks") .build()
assertTrue(result.output.contains("generateQualityReport")) }}TestKit 测试应验证用户可观察行为:任务是否存在、输入输出是否正确、失败信息是否清晰、插件与其他插件组合是否符合预期,而不是只测试实现类内部方法。
19.18 插件测试矩阵
成熟插件至少应覆盖:
| 测试类型 | 验证内容 |
|---|---|
| 单元测试 | Extension、规则类、版本选择和纯函数 |
| 功能测试 | 真实 Gradle 项目中的插件应用和任务执行 |
| 组合测试 | 与 Java、Kotlin、Maven Publish 等插件组合 |
| 失败测试 | 缺失配置、错误版本、错误属性和无效输入 |
| 增量测试 | 输入未变化时是否跳过,输入变化后是否重新执行 |
| 缓存测试 | 任务是否能安全使用 Build Cache |
| 兼容性测试 | 支持的 Gradle、JDK 和插件版本范围 |
测试样例项目应尽量小,每个样例只验证一个行为。把所有场景塞进一个巨大测试工程,会让失败原因和构建耗时都难以判断。
19.19 插件发布与 Plugin Marker
发布插件通常需要同时处理实现模块和 Plugin Marker:
- 实现模块包含插件类、Extension、Task 和资源。
- Plugin Marker 让消费者可以通过插件 ID 和版本解析实现模块。
- Maven 或企业插件仓库需要发布完整 POM、模块元数据和相关构件。
- 公开插件还需检查插件门户的命名、签名、文档和版本策略。
发布前验证:
.\gradlew.bat pluginUnderTestMetadata.\gradlew.bat test.\gradlew.bat publishToMavenLocal使用一个干净的临时项目从仓库消费已发布插件,验证 plugins {} 解析、插件应用和任务行为。不要只在插件源码工程内通过 withPluginClasspath() 就认为发布配置一定正确。
19.20 插件兼容性与升级
插件应明确支持矩阵:
插件版本 → Gradle 版本范围 → JDK 版本范围 → 外部插件版本范围升级 Gradle 或插件时检查:
- 使用的 API 是否仍然是公开 API。
- Configuration、Task、Provider 和属性类型是否有行为变化。
- Kotlin DSL 访问器和预编译脚本是否仍能编译。
- Configuration Cache、Build Cache 和项目隔离是否仍然兼容。
- TestKit 测试是否覆盖旧版本和目标版本。
插件版本升级应先在独立测试矩阵中验证,再推广到所有消费者仓库。
19.21 插件中的 Provider 与配置缓存
插件不要在配置阶段过早读取值:
abstract class ReportExtension { abstract val outputDirectory: DirectoryProperty}
tasks.register<GenerateReportTask>("generateReport") { outputDirectory.set(extension.outputDirectory)}避免:
// 不推荐:配置阶段立即读取并固定结果val output = extension.outputDirectory.get().asFileProvider 的好处是保持值的惰性、连接任务输入输出并减少配置缓存不兼容。插件还应避免保存 Project、Task 或可变模型到全局单例中。
19.22 构建逻辑常见反模式
复制脚本:多个模块复制同一段配置,导致版本和规则漂移。
全局跨项目配置:使用 allprojects、subprojects 和 afterEvaluate 修改所有模块,隐式耦合执行顺序。
万能插件:一个插件应用所有工具、依赖和发布逻辑,导致模块无法选择性使用。
过早执行:插件应用时读取文件、解析依赖、调用外部进程或访问网络。
未声明输入输出:自定义任务能够运行,但增量构建和缓存结果不可信。
依赖内部 API:升级 Gradle 后插件编译或运行失败。
只测试实现类:单元测试通过,但插件在真实构建中没有创建预期任务或产生错误类路径。
19.23 构建逻辑综合示例
目录:
build-logic/├── settings.gradle.kts├── build.gradle.kts└── src/main/kotlin/ ├── company.java-library.gradle.kts └── company.quality.gradle.ktsbuild-logic/build.gradle.kts:
plugins { `kotlin-dsl`}
dependencies { implementation("org.example:quality-gradle-plugin:1.0.0")}company.java-library.gradle.kts:
plugins { `java-library` `maven-publish`}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } withSourcesJar() withJavadocJar()}
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8"}子项目:
plugins { id("company.java-library")}
dependencies { api(project(":api")) implementation(project(":core"))}最终应使用 TestKit 通过一个临时消费者项目验证插件,而不是只检查这些脚本能否被编辑器识别。
20. 属性、Provider API 与懒配置
20.1 属性来源
Gradle 属性可以来自:
- 命令行
-Pname=value。 - 项目
gradle.properties。 - 用户级
~/.gradle/gradle.properties。 - 系统属性。
- 环境变量。
- CI Secret 注入。
20.2 读取 Gradle 属性
val releaseVersion = providers.gradleProperty("releaseVersion") .orElse("0.0.0-SNAPSHOT")命令行:
.\gradlew.bat build -PreleaseVersion=1.2.020.3 读取环境变量
val publishToken = providers.environmentVariable("PUBLISH_TOKEN")不要在配置阶段直接、无条件地调用 .get()。只有实际需要该值时再消费 Provider。
20.4 Provider API
Provider<T> 表示未来可获得的只读值;Property<T> 表示可配置、可连接 Provider 的值。
val outputName = providers.gradleProperty("artifactName") .orElse(project.name) .map { "$it-${project.version}.txt" }Provider API 的价值:
- 延迟计算。
- 自动连接输入输出。
- 改善 Configuration Cache 兼容性。
- 减少配置阶段不必要工作。
- 保留值来源关系。
20.5 懒配置任务
推荐:
val generateReport by tasks.registering { val reportFile = layout.buildDirectory.file("reports/summary.txt") outputs.file(reportFile)
doLast { reportFile.get().asFile.apply { parentFile.mkdirs() writeText("ok") } }}消费任务 Provider:
tasks.named("build") { dependsOn(generateReport)}20.6 register、named 与 configureEach
tasks.register:懒注册新任务。tasks.named:获取已知任务的 Provider。tasks.withType<T>().configureEach:在相关任务需要配置时应用规则。
tasks.withType<Test>().configureEach { useJUnitPlatform()}避免通过 tasks.getByName、.all {} 或遍历全部任务触发不必要的 eager configuration。
20.7 Provider 的组合操作
Provider 的核心价值不只是“延迟读取”,还包括保留值之间的依赖关系:
val artifactName = providers.gradleProperty("artifactName") .orElse(project.name)
val versionName = providers.provider { project.version.toString()}
val artifactFileName = artifactName.zip(versionName) { name, version -> "$name-$version.jar"}常见操作:
map {}:将一个 Provider 的值转换成另一个值。flatMap {}:当转换结果本身是 Provider 时展开嵌套关系。zip():组合多个 Provider,只有在需要结果时才计算。orElse():提供缺省值或备用 Provider。forUseAtConfigurationTime():仅在兼容旧版本或特殊场景时评估,不能作为普遍解决方案。
示例:
val outputDirectory = layout.buildDirectory.dir( providers.gradleProperty("outputDir").orElse("generated"))不要在配置阶段把 Provider 转成普通值后再传递:
// 不推荐:过早求值,丢失 Provider 关系val directory = outputDirectory.get().asFile20.8 Property 的生命周期
Property<T> 是可配置的 Provider,可以连接默认值、用户配置和任务输入:
abstract class ReportExtension { abstract val format: Property<String> abstract val outputDirectory: DirectoryProperty}
val report = extensions.create<ReportExtension>("report")report.format.convention("json")report.outputDirectory.convention(layout.buildDirectory.dir("reports"))属性生命周期通常包含:
- 创建属性。
- 设置默认值
convention。 - 让用户或其他插件调用
set或value覆盖。 - 将属性连接到任务输入或输出。
- 在合适时机固定值,防止后续修改。
固定值的 API:
report.format.finalizeValueOnRead()report.outputDirectory.disallowChanges()finalizeValueOnRead():第一次读取时固定值,适合配置可能延迟完成的场景。finalizeValue():立即固定当前值。disallowChanges():禁止之后继续修改,但不一定立即求值。convention():只设置默认值,不覆盖用户已经设置的值。
插件应明确属性由谁负责设置、何时固定以及缺少值时如何报错。
20.9 set、value 与 convention
三种常见写法表达的意图不同:
val format = objects.property<String>()
format.convention("json")format.set("xml")format.value(providers.gradleProperty("reportFormat"))convention:提供默认值,后续显式设置可以覆盖。set:设置一个值或 Provider,通常表示当前配置明确指定了结果。value:连接一个值或 Provider,并可表达更强的属性关系。
为 Extension 设计 DSL 时,通常在插件内部使用 convention,在消费者脚本中使用赋值或 set。不要在插件应用过程中反复覆盖用户已经设置的属性。
20.10 文件、目录与类路径的惰性 API
Gradle 提供类型安全的文件 API:
val generatedDirectory = layout.buildDirectory.dir("generated/sources")val generatedFile = layout.buildDirectory.file("generated/version.txt")
val inputDirectory = layout.projectDirectory.dir("schema")任务属性:
abstract class GenerateSourcesTask : DefaultTask() { @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) abstract val schemaDirectory: DirectoryProperty
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty}类路径和文件集合也应尽量保持惰性:
val toolClasspath = configurations.resolvable("toolClasspath")
tasks.register<JavaExec>("runTool") { classpath(toolClasspath) mainClass = "org.example.Tool"}不要在配置阶段调用 .files、.singleFile 或 .asFile 解析大型依赖和目录。让任务在真正执行或需要声明输入时使用 Provider、FileCollection 和 FileSystem API。
20.11 任务输入与输出的 Provider 连接
任务之间应通过属性和输出连接,而不是通过共享可变变量:
abstract class GenerateVersionTask : DefaultTask() { @get:Input abstract val version: Property<String>
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val output = outputFile.get().asFile output.parentFile.mkdirs() output.writeText(version.get()) }}
val generateVersion = tasks.register<GenerateVersionTask>("generateVersion") { version.set(providers.provider { project.version.toString() }) outputFile.set(layout.buildDirectory.file("generated/version.txt"))}消费任务:
tasks.named<JavaCompile>("compileJava") { dependsOn(generateVersion) options.compilerArgs.add("-AprojectVersion=${project.version}")}更完整的实现应将生成文件作为 source、classpath 或明确的文件输入连接到消费任务,使 Gradle 能够识别任务关系和输入变化。
20.12 任务注册与配置规避
推荐的任务配置模式:
val reports = tasks.registering { group = "verification" description = "生成项目报告。"}
tasks.named("check") { dependsOn(reports)}
tasks.withType<Test>().configureEach { useJUnitPlatform()}API 选择:
register:创建新任务但延迟实例化。named:懒获取已知任务。withType<T>().configureEach:只对匹配类型的任务应用规则。configureEach:让配置在目标对象需要时执行。getByName、create、all:在新构建逻辑中通常应谨慎使用。
配置规避不是为了让脚本看起来更复杂,而是避免为不会执行的任务创建对象、读取文件、解析依赖和执行插件逻辑。
20.13 任务集合与插件组合
插件可以监听任务类型,而不依赖具体任务名称:
class CompanyTestPlugin : Plugin<Project> { override fun apply(project: Project) { project.tasks.withType<Test>().configureEach { useJUnitPlatform() maxParallelForks = 2 } }}如果目标任务由另一个插件提供,应监听插件应用:
project.pluginManager.withPlugin("java") { project.tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8" }}这样可以避免插件应用顺序依赖,也不会在目标插件未应用的项目中创建无意义的配置。
20.14 Provider 与项目属性
项目属性、系统属性和环境变量都可以转换为 Provider:
val releaseVersion = providers.gradleProperty("releaseVersion") .orElse("0.0.0-SNAPSHOT")
val ciBuild = providers.environmentVariable("CI") .map { it == "true" } .orElse(false)
val signingRequired = providers.gradleProperty("signingRequired") .map(String::toBoolean) .orElse(ciBuild)在任务中消费:
tasks.register("verifyRelease") { onlyIf { signingRequired.get() }}注意:
- 不要将 Secret 作为普通字符串打印到日志。
- 不要在配置阶段无条件读取可选属性并立即失败,除非当前任务确实需要它。
- 对用户输入进行格式校验,并在任务执行前提供清晰错误。
- Provider 链中尽量保持类型转换和默认值逻辑集中。
20.15 Provider 与 onlyIf、enabled 和任务条件
任务是否执行可以由 Provider 驱动:
val skipQuality = providers.gradleProperty("skipQuality") .map(String::toBoolean) .orElse(false)
tasks.named("qualityCheck") { onlyIf { !skipQuality.get() }}onlyIf 和 enabled 的区别需要明确:
onlyIf在任务执行前判断条件,任务可能显示为跳过。enabled = false直接禁用任务,通常表示该任务在当前项目不适用。- 条件应尽量基于声明的 Provider、输入和项目配置,不应读取未声明的外部状态。
如果任务输出必须始终存在,不要用 onlyIf 随意跳过;应把可选行为建模为不同任务或明确的构建参数。
20.16 Provider 与任务输入输出注解
自定义任务的属性类型和注解要匹配实际语义:
abstract class PackageReportTask : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sourceFiles: ConfigurableFileCollection
@get:Input abstract val format: Property<String>
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty}常见选择:
@Input:字符串、数字、枚举、布尔值等普通值。@InputFile、@InputFiles:任务读取的文件。@InputDirectory:任务读取的目录。@OutputFile、@OutputDirectory:任务生成的结果。@Classpath:依赖类路径,按类路径语义进行快照。@Nested:嵌套的可追踪对象。
错误的注解会让 Gradle 无法准确判断任务是否过期,也会导致缓存命中错误。每个输入都应真正影响输出,每个输出都应由任务负责生成。
20.17 Configuration Cache 兼容性
使用 Configuration Cache 验证构建:
.\gradlew.bat :app:build --configuration-cache常见不兼容原因:
- 在任务动作中读取
Project、Gradle或其他项目模型。 - 在配置阶段调用外部进程或访问不可追踪的环境状态。
- 把
Project、Task、FileCollection等可变对象保存到全局单例。 - 任务依赖未声明的文件、系统属性或网络响应。
- 自定义插件在多个项目之间共享可变状态。
更安全的模式:
abstract class PrintEnvironmentTask : DefaultTask() { @get:Input abstract val environmentName: Property<String>
@TaskAction fun printEnvironment() { logger.lifecycle("environment=${environmentName.get()}") }}
val environmentName = providers.environmentVariable("APP_ENV") .orElse("local")
tasks.register<PrintEnvironmentTask>("printEnvironment") { this.environmentName.set(environmentName)}把外部状态转换为 Provider 或显式任务输入,让 Gradle 能够知道构建结果依赖什么。
20.18 ValueSource 与外部值
当需要从外部系统计算值时,可以考虑 ValueSource,而不是在配置脚本中直接执行复杂逻辑:
abstract class GitCommitValueSource : ValueSource<String, ValueSourceParameters.None> { override fun obtain(): String? { return System.getenv("GIT_COMMIT") ?: "unknown" }}
val gitCommit = providers.of(GitCommitValueSource::class) {}使用:
tasks.register("printCommit") { inputs.property("gitCommit", gitCommit) doLast { logger.lifecycle("commit=${gitCommit.get()}") }}ValueSource 仍然要考虑缓存、可复现和安全边界。不要用它掩盖每次构建都变化的未声明外部状态;如果结果影响产物,就应让它成为明确的任务输入。
20.19 Provider 常见错误
过早调用 .get()
配置阶段立即求值会丢失惰性,并可能让不相关任务也触发文件、依赖或环境读取。
把 Provider 当成普通值保存
如果属性之后仍可能被覆盖,应一直保存 Provider 或 Property,而不是保存第一次读取的结果。
循环 Provider
一个 Property 的值依赖另一个 Provider,而后者又依赖前者,会导致无法求值或结果不明确。保持依赖方向单向。
缺少默认值
可选配置应使用 orElse 或 convention;必需配置应在任务真正需要时给出清晰的失败信息。
任务输出未声明
任务虽然写出了文件,但 Gradle 不知道它是输出,导致增量执行、清理和缓存行为不正确。
配置阶段调用外部进程
将外部命令移动到 Exec、JavaExec 或自定义任务中,并声明其输入、输出和环境。
20.20 Provider 综合示例
abstract class GenerateBuildInfoTask : DefaultTask() { @get:Input abstract val applicationName: Property<String>
@get:Input abstract val applicationVersion: Property<String>
@get:Input abstract val gitCommit: Property<String>
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val output = outputFile.get().asFile output.parentFile.mkdirs() output.writeText( """ name=${applicationName.get()} version=${applicationVersion.get()} commit=${gitCommit.get()} """.trimIndent() ) }}
val gitCommit = providers.environmentVariable("GIT_COMMIT") .orElse("local")
val generateBuildInfo = tasks.register<GenerateBuildInfoTask>("generateBuildInfo") { applicationName.set(project.name) applicationVersion.set(providers.provider { project.version.toString() }) this.gitCommit.set(gitCommit) outputFile.set(layout.buildDirectory.file("generated/build-info.properties"))}
tasks.named("processResources") { dependsOn(generateBuildInfo)}这个示例体现了几个原则:
- 外部值通过 Provider 进入任务。
- 每个影响结果的值都声明为
@Input。 - 生成文件声明为
@OutputFile。 - 任务使用
register创建,并通过 Provider 连接路径。 processResources通过任务依赖消费生成结果,而不是在配置阶段直接写文件。
21. 增量构建与任务缓存
21.1 什么是增量构建
增量构建的基本思想是:如果任务的输入和输出没有发生影响结果的变化,就不重复执行任务。
Gradle 可能报告以下状态:
UP-TO-DATE:输入没有变化,已有输出仍有效。FROM-CACHE:从构建缓存恢复输出。NO-SOURCE:没有可处理的源文件。SKIPPED:任务条件不满足或被排除。EXECUTED:任务实际执行。FAILED:任务执行失败。
21.2 自定义任务声明输入输出
abstract class GenerateMessage : DefaultTask() { @get:Input abstract val message: Property<String>
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val file = outputFile.get().asFile file.parentFile.mkdirs() file.writeText(message.get()) }}
tasks.register<GenerateMessage>("generateMessage") { message.set(providers.gradleProperty("message").orElse("hello")) outputFile.set(layout.buildDirectory.file("generated/message.txt"))}常用输入输出注解:
@Input:简单值。@InputFile、@InputDirectory:文件输入。@InputFiles:文件集合输入。@OutputFile、@OutputDirectory:输出位置。@OutputFiles:多个输出。@Internal:不参与输入输出快照。@Nested:嵌套的可追踪对象。
输入输出必须完整、稳定、可序列化。把真正影响结果的参数标成 @Internal 会造成错误缓存;把临时目录等无关数据标成输入会降低缓存命中率。
21.3 文件集合和路径规范化
abstract class CopyTemplate : DefaultTask() { @get:InputDirectory abstract val sourceDirectory: DirectoryProperty
@get:OutputDirectory abstract val destinationDirectory: DirectoryProperty
@TaskAction fun copy() { project.copy { from(sourceDirectory) into(destinationDirectory) } }}文件输入应通过 Gradle 的 DirectoryProperty、RegularFileProperty、ConfigurableFileCollection 等类型表达,避免在配置阶段把文件内容读进普通字符串。
21.4 为什么 clean 不是默认修复方案
clean 会删除 build/ 输出,从而破坏增量构建和本地缓存收益。若必须频繁 clean build 才能通过,通常要检查:
- 任务输入输出声明是否不完整。
- 任务是否写入了未声明目录。
- 代码生成目录是否和源码目录混用。
- 第三方插件是否产生不稳定输出。
- 构建脚本是否依赖当前时间、随机数或本机路径。
21.5 Up-to-date 检查的基本过程
Gradle 会根据任务实现、输入、输出和规范化规则计算任务状态。只有当当前输出仍然代表相同输入结果时,任务才可能显示 UP-TO-DATE。
影响判断的因素包括:
- 任务实现类或插件实现变化。
@Input、@InputFile、@InputDirectory等输入变化。- 输出文件缺失、被修改或目录结构变化。
- 文件路径敏感性和内容规范化方式变化。
- 任务依赖的类路径、工具版本或嵌套属性变化。
- 任务条件、构建参数或执行环境变化。
.\gradlew.bat generateMessage --info.\gradlew.bat generateMessage --rerun--rerun 适合验证任务逻辑和输入输出声明,不应作为日常构建参数。
21.6 文件路径敏感性
abstract class PackageSourcesTask : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sourceFiles: ConfigurableFileCollection
@get:OutputFile abstract val archive: RegularFileProperty}常用策略:
ABSOLUTE:绝对路径变化会导致任务失效。RELATIVE:相对于输入根目录的路径参与快照,通常更适合可重定位任务。NAME_ONLY:只比较文件名,适合明确不关心目录结构的输入。NONE:忽略路径,只比较内容,必须确认任务不依赖路径。
路径敏感性过强会降低跨机器缓存命中率,过弱则可能把不同文件误判为相同输入。不能为了提高命中率而盲目使用 NONE。
21.7 输入规范化与无关文件
如果任务只处理特定文件,应在输入集合中明确过滤:
abstract class CompileTemplatesTask : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val templates: ConfigurableFileCollection}
val compileTemplates = tasks.register<CompileTemplatesTask>("compileTemplates") { templates.from( layout.projectDirectory.dir("src/templates").asFileTree.matching { include("**/*.mustache") exclude("**/*.bak") } )}不要把整个项目目录作为输入,再在任务内部过滤大量无关文件。这样会让任意无关文件变化都使任务失效,并增大输入快照和缓存键。
21.8 @Classpath 与工具版本
编译器、代码生成器和格式化工具的类路径通常使用 @Classpath:
abstract class RunGeneratorTask : DefaultTask() { @get:Classpath abstract val generatorClasspath: ConfigurableFileCollection
@get:Input abstract val entryPoint: Property<String>
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty}工具版本变化通常应使任务失效。不要把整个依赖目录随意声明为 @InputFiles,应根据任务真正使用的工具类路径和配置文件确定输入边界。
21.9 可缓存任务与 @CacheableTask
只有输入、输出和实现足够确定时,才适合启用构建缓存:
@CacheableTaskabstract class GenerateMessageTask : DefaultTask() { @get:Input abstract val message: Property<String>
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val output = outputFile.get().asFile output.parentFile.mkdirs() output.writeText(message.get()) }}可缓存任务必须满足:
- 所有影响输出的输入都已声明。
- 输出只写入声明的输出路径。
- 相同输入在不同机器上产生等价输出。
- 不依赖未声明的时间、随机数、用户目录、网络响应或本机环境。
- 输出目录不会被其他任务同时写入。
无法满足确定性要求的任务应保持不可缓存,并说明原因:
@DisableCachingByDefault(because = "任务依赖外部服务的实时响应")abstract class QueryExternalServiceTask : DefaultTask()21.10 输出目录与任务隔离
每个任务应拥有独立的输出目录或输出文件:
val generateA = tasks.register("generateA") { outputs.dir(layout.buildDirectory.dir("generated/a"))}
val generateB = tasks.register("generateB") { outputs.dir(layout.buildDirectory.dir("generated/b"))}不要让多个任务写入同一个未声明的共享目录,否则可能出现输出互相删除、并行竞态、快照污染和缓存恢复不完整。多个任务需要共享结果时,应由一个生产任务声明输出,其他任务通过文件输入或项目变体消费。
21.11 增量构建与 Build Cache 的区别
- 增量构建:当前机器有上一次任务输出,根据输入输出比较决定是否跳过任务。
- Build Cache:当前机器没有可直接复用的输出时,从本地或远程缓存恢复其他构建产生的结果。
常见状态:
UP-TO-DATE 当前输出有效,任务不执行FROM-CACHE 从构建缓存恢复输出EXECUTED 任务实际执行并产生输出构建缓存不是增量构建的替代品。输入输出声明不完整时,缓存可能保存错误结果;输出不确定时,跨机器复用会放大问题。
21.12 启用本地与远程 Build Cache
org.gradle.caching=trueSettings 配置:
buildCache { local { isEnabled = true }
remote<HttpBuildCache> { url = uri("https://cache.example.com/cache/") isEnabled = providers.environmentVariable("CI").isPresent isPush = providers.environmentVariable("CI").isPresent credentials { username = providers.gradleProperty("cacheUser").orNull password = providers.gradleProperty("cachePassword").orNull } }}推荐让开发机主要读取缓存,由受信任的 CI 在完整验证后推送共享缓存。缓存凭据必须使用 Secret,不能写入仓库。
21.13 缓存键与可重定位性
缓存键由任务实现、输入、输出规范化和相关参数共同决定。可重定位任务应避免把绝对路径写入输出:
不推荐:/home/user/project/build/generated/file.txt推荐: build/generated/file.txt还应固定文件排序、文本编码、换行符和时区,不要把用户名、临时目录、当前时间和未声明随机值写入产物。PathSensitivity.RELATIVE 只能改善输入路径快照,不能自动清理输出中的绝对路径。
21.14 缓存未命中诊断
.\gradlew.bat build -Dorg.gradle.caching.debug=true --info诊断重点:
- 任务是否声明为可缓存。
- 哪个输入属性发生变化。
- 文件内容、路径或规范化方式是否变化。
- 任务实现、插件或工具版本是否变化。
- 是否存在未声明的输出、外部状态或工作目录差异。
- 远程缓存是否启用、凭据是否有效、服务是否可访问。
缓存命中率不是越高越好。通过忽略真实输入提高命中率,可能得到错误产物。
21.15 远程缓存的安全边界
远程缓存会把任务输出提供给其他构建使用:
- 只允许可信构建推送缓存。
- 分离不同分支、兼容矩阵和信任级别的缓存命名空间。
- 不要缓存包含 Secret、用户数据或内部临时文件的任务。
- 验证缓存服务的 TLS、权限和凭据。
- 缓存不是长期制品仓库,正式 JAR 和分发包仍应发布到制品仓库。
21.16 增量任务与文件变化
处理大量文件的任务应明确处理新增、修改和删除文件,并保证删除输入时能够清理对应输出。增量处理不能替代完整输入输出声明;任务依赖的全局配置、工具版本和模板目录仍需声明为输入。
如果任务输出与输入文件一一对应,可以使用 Gradle 的增量输入能力减少工作量;如果任务是全量聚合生成,则应保持简单的完整输入快照,避免为了增量化引入错误状态。
21.17 非确定性输出治理
以下因素会破坏增量构建和缓存:
- 将当前时间写入输出。
- 使用未固定种子的随机数。
- 依赖操作系统目录遍历顺序。
- 依赖本机默认字符集、时区或换行符。
- 把绝对路径写入生成文件。
- 从未声明的网络服务读取数据。
- 读取用户主目录、临时目录或未声明环境变量。
治理方式是把必要的时间、随机种子、时区和编码变成显式输入,固定输入排序和输出格式,并将网络请求移到独立的不可缓存任务。
21.18 clean、重新执行与缓存清理
.\gradlew.bat assemble.\gradlew.bat assemble --rerun.\gradlew.bat clean assemble.\gradlew.bat clean assemble --no-build-cache- 普通执行:验证正常的增量和缓存行为。
--rerun:强制当前任务执行,适合验证任务逻辑。clean:删除项目输出,适合排查残留输出或目录污染。--no-build-cache:临时跳过 Build Cache,帮助区分缓存问题和任务逻辑问题。
不要把这些参数固化到日常脚本中,否则会掩盖错误的任务模型并降低构建性能。
21.19 缓存任务的测试方法
可缓存任务至少应测试:
- 第一次执行生成正确输出。
- 输入不变时显示
UP-TO-DATE。 - 删除输出后能够从缓存恢复或重新执行。
- 修改每一个输入后任务都会失效。
- 修改无关文件不会使任务失效。
- 换到不同工作目录后仍能正确执行或命中可重定位缓存。
- 任务失败时不会留下可被错误复用的部分输出。
.\gradlew.bat generateMessage.\gradlew.bat generateMessage --infoRemove-Item -Recurse -Force build/generated.\gradlew.bat generateMessage --info清理命令只应在明确的测试工程或临时目录中执行。
21.20 增量任务综合示例
@CacheableTaskabstract class GenerateBuildInfoTask : DefaultTask() { @get:Input abstract val applicationName: Property<String>
@get:Input abstract val applicationVersion: Property<String>
@get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) abstract val templateDirectory: DirectoryProperty
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val template = templateDirectory.file("info.template").get().asFile val output = outputFile.get().asFile output.parentFile.mkdirs() output.writeText( template.readText() .replace("{{name}}", applicationName.get()) .replace("{{version}}", applicationVersion.get()) ) }}
val generateBuildInfo = tasks.register<GenerateBuildInfoTask>("generateBuildInfo") { applicationName.set(project.name) applicationVersion.set(providers.provider { project.version.toString() }) templateDirectory.set(layout.projectDirectory.dir("src/build-info")) outputFile.set(layout.buildDirectory.file("generated/build-info.txt"))}
tasks.named("processResources") { dependsOn(generateBuildInfo)}目录:
src/build-info/info.templatebuild/generated/build-info.txt该任务的输入是应用名、版本和模板目录,输出是生成文件。输入不变时可以跳过执行,任务确定性满足要求时还可以跨构建复用缓存。
22. Build Cache
22.1 构建缓存是什么
Build Cache 保存可缓存任务的输出。当另一次构建具有等价输入时,可以直接恢复结果,减少编译、测试前处理和代码生成时间。
它与增量构建不同:
| 机制 | 复用范围 | 依赖什么 |
|---|---|---|
| Up-to-date | 当前项目、当前输出 | 本地任务历史与输出 |
| Build Cache | 当前机器或远程机器 | 输入快照与缓存条目 |
| Configuration Cache | 构建配置阶段 | 配置模型与兼容性 |
22.2 启用本地缓存
命令行:
.\gradlew.bat build --build-cache项目属性:
org.gradle.caching=trueSettings 中也可以配置:
buildCache { local { isEnabled = true }}22.3 远程缓存
远程缓存适合 CI 和大型团队共享,但需要考虑身份认证、缓存条目可信度、分支隔离、环境一致性和敏感信息泄露。
生产环境通常要区分只读请求和写入权限。来自不可信代码的构建不应拥有向共享缓存写入任意结果的权限。
22.4 任务可缓存的前提
任务必须满足:
- 输入能完整表示任务行为。
- 输出路径和内容可预测。
- 没有隐藏的网络、时间、随机数或本机状态依赖。
- 输出不包含密钥、用户目录或机器特有内容。
- 任务类型和实现具有稳定身份。
不确定任务是否安全缓存时,应先禁用该任务的缓存,而不是冒险复用错误结果。
22.5 本地 Build Cache 的工作方式
本地 Build Cache 通常位于 Gradle User Home 下,用于保存当前机器或用户范围内的任务输出。启用方式:
.\gradlew.bat build --build-cache或:
org.gradle.caching=true本地缓存的特点:
- 不需要网络,适合开发机快速重复构建。
- 受本机 Gradle User Home、缓存清理和磁盘空间影响。
- 通常不适合作为团队之间共享结果的唯一方式。
- 缓存命中时任务显示
FROM-CACHE,而不是UP-TO-DATE。
清理项目输出不会自动等价于清理所有 Build Cache。调试时应区分 build/ 输出、Gradle User Home 缓存和远程缓存服务。
22.6 远程 Build Cache 配置
远程缓存可以让 CI 和开发机构建共享经过验证的任务输出:
buildCache { local { isEnabled = true }
remote<HttpBuildCache> { url = uri("https://cache.example.com/cache/") isEnabled = true isPush = false credentials { username = providers.gradleProperty("cacheUser").orNull password = providers.gradleProperty("cachePassword").orNull } }}isPush 表示当前构建是否可以向远程缓存写入结果。推荐:
- 普通开发构建只读远程缓存。
- 受信任的 CI 构建在测试、静态检查和产物验证通过后推送缓存。
- 不可信分支、外部贡献者或未审查代码不允许写入共享缓存。
- 缓存服务凭据使用 CI Secret,不能写入项目文件。
- 缓存服务不可用时,构建是否失败应根据团队的可靠性要求决定。
22.7 缓存键包含什么
Build Cache 通过任务缓存键判断某个输出能否复用。缓存键通常受到以下因素影响:
- 任务类型和任务实现。
- 任务输入值、输入文件内容和路径规范化。
- 任务输出声明和输出规范化。
- 工具类路径、编译器版本和插件实现。
- 任务相关的嵌套属性、依赖关系和构建参数。
因此,缓存命中不是简单的“文件内容相同”。如果任务输出还依赖未声明的当前时间、环境变量或网络响应,Gradle 无法从缓存键中判断这些变化,可能复用错误结果。
可缓存任务应保证:
相同的缓存键 → 等价的任务输出不同的有效输入 → 不会错误复用旧输出22.8 @CacheableTask 与缓存契约
自定义任务通过 @CacheableTask 表示其输出可以被 Build Cache 复用:
@CacheableTaskabstract class GenerateDescriptorTask : DefaultTask() { @get:Input abstract val moduleName: Property<String>
@get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sourceFiles: ConfigurableFileCollection
@get:OutputFile abstract val descriptorFile: RegularFileProperty
@TaskAction fun generate() { val output = descriptorFile.get().asFile output.parentFile.mkdirs() output.writeText( "module=${moduleName.get()}\n" + "sources=${sourceFiles.files.size}\n" ) }}启用缓存前必须检查:
- 输入是否完整表达任务行为。
- 输出是否全部声明并且只写入声明位置。
- 输出是否与工作目录、用户名、临时路径无关。
- 任务是否依赖隐藏的网络、时间、随机数或操作系统状态。
- 任务是否可能产生包含密码、Token 或内部数据的输出。
22.9 不应缓存的任务
以下任务通常不适合直接推送到共享 Build Cache:
- 依赖实时外部服务结果的任务。
- 输出包含 Secret、个人路径或机器信息的任务。
- 生成不可复现的时间戳、随机 ID 或临时凭证的任务。
- 修改外部数据库、对象存储或其他不可声明副作用的任务。
- 输出受到本机安装软件、系统时间或未声明环境影响的任务。
- 任务逻辑依赖未被输入快照记录的全局状态。
可以使用 @DisableCachingByDefault 明确禁用默认缓存:
@DisableCachingByDefault(because = "任务依赖外部服务的实时响应")abstract class PublishExternalTask : DefaultTask()禁用缓存不是失败,而是诚实表达任务契约。后续只有在补齐输入、输出和确定性保证后,才应该重新启用缓存。
22.10 本地与远程缓存的使用策略
一个稳妥的团队策略:
| 构建来源 | 读取缓存 | 推送缓存 | 目的 |
|---|---|---|---|
| 开发机 | 本地和远程 | 通常关闭 | 快速复用可信结果 |
| 受信任主分支 CI | 本地和远程 | 开启 | 生成团队共享结果 |
| 外部贡献分支 | 本地或只读远程 | 关闭 | 防止污染共享缓存 |
| 发布构建 | 只读或受控推送 | 按发布策略 | 保障结果可审查 |
缓存不能替代正式制品仓库。JAR、ZIP、Docker 镜像和发布报告应通过正式制品发布流程保存,并具有版本、签名和审计信息。
22.11 缓存未命中诊断
启用缓存调试:
.\gradlew.bat :app:compileJava --build-cache --info.\gradlew.bat :app:compileJava -Dorg.gradle.caching.debug=true --info排查顺序:
- 确认任务类型是否可缓存。
- 确认 Build Cache 是否启用,远程缓存 URL 是否正确。
- 查看任务是
EXECUTED、UP-TO-DATE还是FROM-CACHE。 - 检查缓存键相关的输入、实现、工具类路径和路径规范化。
- 检查缓存服务访问权限、凭据、TLS 和服务日志。
- 在干净的 Gradle User Home 或不同工作目录中复现。
- 比较缓存命中构建与完整执行构建的输出内容。
缓存未命中本身不是错误。重要的是能够解释未命中原因,并确认缓存命中时不会改变构建结果。
22.12 缓存调试与任务输入
如果任务没有命中缓存,可以临时执行:
.\gradlew.bat generateDescriptor --rerun.\gradlew.bat generateDescriptor --no-build-cache --info两次执行的意义不同:
--rerun强制任务执行,但仍可能参与缓存写入。--no-build-cache禁止读取和写入 Build Cache,用于隔离缓存问题。
之后应检查:
- 任务输入是否包含所有影响结果的属性。
- 输入文件是否使用了正确的路径敏感性。
- 是否有任务在输出目录中写入未声明的文件。
- 任务实现或插件版本是否发生变化。
- 输出是否包含不稳定排序、时间戳或绝对路径。
不要通过把输入标记为 @Internal 来“修复”缓存未命中。这样可能让缓存结果失去正确性。
22.13 可重定位任务
可重定位任务可以在不同工作目录或不同机器上复用缓存。实现时要注意:
- 输入使用相对路径规范化。
- 输出不包含绝对工作区路径。
- 归档文件中的文件顺序稳定。
- 文本编码、换行和时区明确固定。
- 工具类路径和版本显式声明。
- 不读取用户目录、临时目录或未声明环境变量。
例如,生成报告时不要写入:
projectDir=/Users/alice/work/demo而应写入与项目无关的相对信息,或者把工作目录作为明确输入并接受缓存无法跨目录复用。
22.14 远程缓存安全
远程缓存可能向不同构建提供任务输出,因此需要建立信任边界:
- 仅可信 CI 可以推送共享缓存。
- 外部贡献分支使用只读缓存或隔离缓存。
- 缓存服务需要认证、TLS、最小权限和审计日志。
- 不缓存包含凭据、内部路径、个人信息或未脱敏日志的任务。
- 不同 JDK、操作系统或插件兼容矩阵需要使用不同缓存边界。
- 缓存失效、清理和撤销策略要可执行。
如果怀疑缓存中存在错误或恶意结果,应先停止推送和消费,再清理受影响命名空间并重新执行可信构建。
22.15 CI 中的 Build Cache
CI 可以将 Build Cache 作为流水线加速层:
# 示例流程伪代码steps: - checkout - setup-java - run: ./gradlew check --build-cache - run: ./gradlew assemble --build-cacheCI 设计建议:
- 固定 Wrapper、JDK、操作系统和插件版本。
- 让验证通过的构建推送缓存,不让未审查分支推送共享缓存。
- 缓存凭据使用 Secret,并限制写入权限。
- 缓存命中后仍执行必要的测试和结果验证。
- 记录缓存命中率、未命中原因和缓存服务状态。
- 缓存不可用时保留不依赖缓存完成构建的回退路径。
22.16 缓存性能评估
评估 Build Cache 不能只看命中率,还要观察:
- 缓存请求延迟和上传下载时间。
- 命中节省的任务执行时间。
- 缓存条目大小和网络流量。
- 本地磁盘占用和清理成本。
- 缓存未命中的主要原因。
- 缓存服务故障对构建耗时和成功率的影响。
小任务的缓存输出可能比重新执行更小成本,盲目缓存所有任务反而会增加网络开销。应优先缓存编译、代码生成、测试前处理和耗时稳定的任务。
22.17 Build Cache 综合示例
settings.gradle.kts:
buildCache { local { isEnabled = true }
remote<HttpBuildCache> { url = uri("https://cache.example.com/cache/") isEnabled = providers.environmentVariable("CI").isPresent isPush = providers.environmentVariable("CI").isPresent && providers.environmentVariable("CACHE_PUSH").orNull == "true" credentials { username = providers.gradleProperty("cacheUser").orNull password = providers.gradleProperty("cachePassword").orNull } }}gradle.properties:
org.gradle.caching=true构建和诊断:
.\gradlew.bat check --build-cache --info.\gradlew.bat build -Dorg.gradle.caching.debug=true --info.\gradlew.bat build --no-build-cache上线前应使用两个独立工作目录验证:
- 第一个目录完整执行并产生缓存。
- 第二个目录使用相同 Wrapper、JDK、依赖和源码执行。
- 检查耗时任务是否显示
FROM-CACHE。 - 对比两个目录的关键产物内容和校验和。
- 修改一个真实输入,确认任务失效并重新执行。
22.18 本章小结
Build Cache 的核心不是“把任务结果存起来”,而是建立可信的可复用契约:
- 用完整输入输出声明构造正确的缓存键。
- 用
@CacheableTask只缓存确定性、可重定位的任务。 - 用本地缓存加速开发,用远程缓存服务共享可信结果。
- 让不可信构建只读缓存,避免污染团队共享缓存。
- 用缓存调试日志分析未命中,而不是盲目修改注解。
- 固定路径、排序、编码、时区、工具版本和输出格式。
- 不把 Build Cache 当作正式制品仓库或安全证明。
- 在 CI 和独立工作目录中验证缓存命中后的产物等价性。
当缓存结果可以被解释、验证、撤销和重新生成时,Build Cache 才是可靠的工程加速机制,而不是隐藏构建问题的黑盒。
23. Configuration Cache
23.1 它解决什么问题
Configuration Cache 缓存一次构建的配置结果。后续运行相同或相近的任务请求时,Gradle 可以跳过部分初始化和配置阶段,尤其适合大型多项目构建。
启用:
.\gradlew.bat build --configuration-cache或:
org.gradle.configuration-cache=true23.2 与 Build Cache 的区别
Configuration Cache:减少配置项目和任务模型的时间Build Cache:复用任务执行后的输出二者可以同时使用,也可以单独启用。
23.3 常见不兼容原因
- 任务动作捕获
Project、Gradle或其他不可序列化对象。 - 在任务执行阶段访问构建服务之外的 Project API。
- 配置阶段直接读取动态环境并改变模型。
- 插件使用旧式全局状态。
- 自定义任务没有通过 Provider、Property 和文件 API 表达输入。
- 构建脚本在任务执行时依赖配置阶段的临时对象。
23.4 修复方向
abstract class PrintVersion : DefaultTask() { @get:Input abstract val versionText: Property<String>
@TaskAction fun printVersion() { logger.lifecycle("version=${versionText.get()}") }}
tasks.register<PrintVersion>("printVersion") { versionText.set(providers.provider { project.version.toString() })}更进一步,应避免在任务类中持有 Project 引用,使用注入的服务、文件系统 API、Worker API 和 Property 类型。
23.5 采用策略
- 在单模块项目或 CI 的一个任务上试运行。
- 记录 Gradle 报告的兼容问题。
- 先修复自定义插件和内部构建逻辑。
- 再处理第三方插件,必要时升级版本。
- 将
--configuration-cache纳入持续验证。
23.6 Configuration Cache 的复用条件
Configuration Cache 不是简单缓存一个配置文件,而是缓存一次构建请求对应的配置结果。后续构建要复用缓存,通常需要满足:
- Gradle Wrapper 和相关构建逻辑没有改变。
- 请求的任务和关键参数相同或兼容。
- Settings、项目模型和插件配置没有产生新的结果。
- 外部输入、Provider 和环境值仍满足缓存记录。
- 构建逻辑没有访问被禁止或无法追踪的全局状态。
第一次执行通常会配置并保存缓存,第二次执行才可能明显变快:
.\gradlew.bat :app:compileJava --configuration-cache.\gradlew.bat :app:compileJava --configuration-cache看到配置缓存被重新计算,不一定意味着失败。任务请求、Wrapper、插件、Settings、Gradle 参数或外部输入变化,都可能使旧缓存失效。
23.7 Configuration Cache 与 Build Cache
二者位于构建流程的不同阶段:
Configuration Cache:复用 Settings、项目配置和任务图Build Cache:复用任务执行后的输出一个构建可以出现以下组合:
| 配置缓存 | 构建缓存 | 结果 |
|---|---|---|
| 命中 | 命中 | 配置和耗时任务都减少执行 |
| 命中 | 未命中 | 跳过配置,但任务仍执行 |
| 未命中 | 命中 | 重新配置,但任务输出可恢复 |
| 未命中 | 未命中 | 重新配置并执行任务 |
排查性能时应分别测量配置阶段、任务执行和缓存访问,不要把两种缓存的效果混为一谈。
23.8 问题报告与诊断参数
执行配置缓存后,Gradle 会报告不兼容问题。可以使用:
.\gradlew.bat build --configuration-cache.\gradlew.bat build --configuration-cache-problems=warn.\gradlew.bat build --no-configuration-cache问题报告通常位于:
build/reports/configuration-cache/诊断时记录:
- 哪个任务或插件触发问题。
- 问题发生在配置阶段还是任务执行阶段。
- 被访问的对象、文件、环境变量或外部服务。
- 是否是内部构建逻辑,还是第三方插件限制。
- 修复后第二次运行是否真正复用了配置缓存。
warn 适合迁移阶段收集问题;CI 门禁应在确认构建逻辑兼容后恢复严格检查,避免问题长期被忽略。
23.9 任务动作中不能依赖 Project
不兼容的写法:
tasks.register("printVersion") { doLast { println(project.version) }}更适合的写法是把值在配置阶段转换为任务输入:
abstract class PrintVersionTask : DefaultTask() { @get:Input abstract val versionText: Property<String>
@TaskAction fun printVersion() { logger.lifecycle("version=${versionText.get()}") }}
tasks.register<PrintVersionTask>("printVersion") { versionText.set(providers.provider { project.version.toString() })}任务执行阶段应只读取自己的属性和注入的服务,不要保存或访问 Project、Gradle、其他 Task 对象和可变项目模型。
23.10 外部文件、环境变量与系统属性
外部值需要显式建模:
val buildProfile = providers.gradleProperty("buildProfile") .orElse(providers.environmentVariable("BUILD_PROFILE")) .orElse("local")
abstract class ProfileTask : DefaultTask() { @get:Input abstract val profile: Property<String>
@TaskAction fun printProfile() { logger.lifecycle("profile=${profile.get()}") }}
tasks.register<ProfileTask>("printProfile") { profile.set(buildProfile)}不要在任务动作中直接调用 System.getenv()、System.getProperty() 或读取用户目录。这样 Gradle 无法清楚知道配置缓存和任务输出依赖哪些外部值。
文件也应通过 Gradle 文件 API 表达:
abstract class ReadConfigTask : DefaultTask() { @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE) abstract val configFile: RegularFileProperty}
tasks.register<ReadConfigTask>("readConfig") { configFile.set(layout.projectDirectory.file("config/build.properties"))}23.11 外部进程与文件操作
不要在配置阶段直接执行外部进程:
// 不推荐val output = Runtime.getRuntime() .exec("git rev-parse HEAD") .inputStream .bufferedReader() .readText()可以使用专用任务和声明式输入输出:
abstract class GitInfoTask : DefaultTask() { @get:OutputFile abstract val outputFile: RegularFileProperty
@get:TaskAction fun writeInfo() { outputFile.get().asFile.apply { parentFile.mkdirs() writeText("commit=provided-by-ci\n") } }}需要执行外部程序时,优先使用 Exec、JavaExec、ExecOperations 或 Worker API,并声明命令参数、环境、输入和输出。配置缓存关注的是“如何建模外部状态”,不是禁止所有外部程序。
23.12 Build Service 与共享资源
多个任务需要共享连接池、限流器或客户端时,可以使用 Build Service,而不是全局单例:
abstract class HttpClientService : BuildService<BuildServiceParameters.None>, AutoCloseable { override fun close() { // 关闭客户端资源 }}
val httpClient = gradle.sharedServices.registerIfAbsent( "httpClient", HttpClientService::class)
tasks.withType<SomeNetworkTask>().configureEach { usesService(httpClient)}Build Service 应注意:
- 声明合理的参数和生命周期。
- 控制最大并发数,避免任务同时访问外部服务。
- 不把不可序列化的 Project 或 Task 对象保存到服务参数中。
- 在
close中释放资源。 - 把服务使用关系通过
usesService连接到任务。
23.13 跨项目访问与配置缓存
以下做法容易破坏配置缓存和项目隔离:
// 不推荐:执行阶段访问另一个项目模型abstract class CrossProjectTask : DefaultTask() { @TaskAction fun run() { project.project(":core").tasks.named("jar").get().execute() }}更合理的方式:
- 用
implementation(project(":core"))表达项目依赖。 - 用项目变体共享构件。
- 用聚合任务连接任务图,而不是手动执行另一个 Task。
- 用 Convention Plugin 在配置阶段声明统一规则。
- 让任务只消费已声明的文件、Provider 和 Build Service。
跨项目访问越隐式,越难同时兼容 Configuration Cache、并行执行和 Isolated Projects。
23.14 插件兼容性治理
第三方插件可能在配置阶段访问内部状态或在任务动作中保存 Project 引用。迁移时按以下顺序处理:
- 升级到支持当前 Gradle 的插件版本。
- 查看插件发布说明和已知配置缓存问题。
- 在最小示例项目中单独应用插件并运行配置缓存。
- 将插件应用范围缩小到确实需要的项目。
- 对无法立即升级的插件建立隔离边界或暂时禁用配置缓存。
- 在 CI 中记录插件版本和配置缓存报告。
不要为了消除报告而随意关闭问题检查。应判断问题是否会造成错误缓存、跨项目状态泄漏或构建结果不一致。
23.15 多项目迁移策略
大型多项目工程可以分层迁移:
第一阶段:根项目和一个核心模块 ↓第二阶段:Convention Plugin 与自定义 Task ↓第三阶段:所有 Java/Kotlin 模块 ↓第四阶段:发布、测试、代码生成和 CI 任务每个阶段都验证:
- 第一次运行能生成配置缓存。
- 第二次相同任务请求能够复用配置缓存。
- 修改 Settings、插件、Provider 输入后会正确失效。
- 任务输出、Build Cache 和发布产物没有变化。
- 第三方插件问题已经记录并有升级计划。
不要在所有模块同时打开严格门禁后再开始排错,这会让问题数量和来源难以区分。
23.16 CI 中的 Configuration Cache
CI 可以把配置缓存作为性能优化,但应固定构建请求和环境:
.\gradlew.bat check --configuration-cache.\gradlew.bat assemble --configuration-cacheCI 建议:
- 固定 Wrapper、JDK、操作系统镜像和关键环境变量。
- 不在同一个工作目录中混用互不兼容的任务请求。
- 保存配置缓存问题报告和构建扫描信息。
- 新增插件或构建逻辑时增加配置缓存验证任务。
- 对外部贡献分支和受信任发布分支使用一致但权限隔离的流程。
- 配置缓存失败时保留明确的失败信息和回退诊断命令。
Configuration Cache 不等同于 CI 缓存目录。CI 是否保留 .gradle 内容、缓存是否跨执行器共享,应由流水线缓存策略单独决定。
23.17 常见问题排查
1. 第二次运行仍然重新配置
检查任务请求、Gradle 参数、Wrapper、Settings、插件版本和外部 Provider 是否变化,并查看 build/reports/configuration-cache/ 报告。
2. 任务动作报 Project 访问错误
把任务需要的值转换为 @Input、Property、文件属性或注入服务,执行阶段只读取这些对象。
3. 第三方插件导致配置缓存失败
升级插件、查阅官方兼容说明,在最小工程中复现,并决定修复、替换或隔离插件。
4. 环境变量变化没有使构建失效
不要在配置或任务动作中隐式读取环境变量,使用 providers.environmentVariable 并将 Provider 连接到任务或模型属性。
5. 配置缓存命中但结果不正确
检查是否有未声明的文件、网络、时间、随机数、系统属性或跨项目可变状态。配置缓存正确性优先于命中率。
23.18 Configuration Cache 综合示例
abstract class GenerateBuildInfoTask : DefaultTask() { @get:Input abstract val versionText: Property<String>
@get:Input abstract val profile: Property<String>
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val output = outputFile.get().asFile output.parentFile.mkdirs() output.writeText( "version=${versionText.get()}\n" + "profile=${profile.get()}\n" ) }}
val profile = providers.gradleProperty("profile") .orElse(providers.environmentVariable("BUILD_PROFILE")) .orElse("local")
val generateBuildInfo = tasks.register<GenerateBuildInfoTask>("generateBuildInfo") { versionText.set(providers.provider { project.version.toString() }) this.profile.set(profile) outputFile.set(layout.buildDirectory.file("generated/build-info.properties"))}
tasks.named("processResources") { dependsOn(generateBuildInfo)}验证:
.\gradlew.bat processResources --configuration-cache.\gradlew.bat processResources --configuration-cache.\gradlew.bat processResources --configuration-cache -Pprofile=ci.\gradlew.bat processResources --no-configuration-cache第二次相同请求应优先复用配置缓存;切换 profile 后应产生新的配置或任务输入结果,不能继续复用不匹配的状态。
24. Daemon、并行构建与性能分析
24.1 Gradle Daemon
Daemon 是长期运行的 Gradle 后台进程,能够复用 JVM、类加载器和构建服务。
.\gradlew.bat --status.\gradlew.bat --stopCI 是否复用 Daemon 应结合执行器生命周期、内存和安全策略决定。开发机通常保留它可以缩短重复构建启动时间。
24.2 Gradle JVM 参数
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8不要盲目增大堆内存。先观察 GC、构建扫描和任务耗时,再判断瓶颈是内存、配置、依赖下载、编译还是测试。
24.3 并行构建
org.gradle.parallel=true并行执行要求任务之间没有未声明的共享目录、全局变量或顺序依赖。若构建脚本隐式读写同一个临时文件,开启并行后可能暴露隐藏错误。
24.4 性能诊断命令
.\gradlew.bat build --profile.\gradlew.bat build --scan.\gradlew.bat build --info.\gradlew.bat build --debug--profile:生成本地性能报告。--scan:生成更完整的构建诊断,使用前遵守组织数据上传策略。--info:增加常规诊断信息。--debug:输出大量底层日志,适合定位具体问题。
24.5 性能优化优先级
- 先确认是否重复执行了不必要任务。
- 再检查依赖下载和仓库响应时间。
- 观察配置阶段是否因为 eager configuration 过慢。
- 为自定义任务补齐输入输出声明。
- 评估 Build Cache 和 Configuration Cache。
- 最后再调整并行度、JVM 堆和 Worker 数量。
不要把“每次 clean build”当作性能基准;它测量的是最差路径。
24.6 构建性能的三个阶段
Gradle 性能问题通常分布在三个阶段:
Initialization → Configuration → Execution- Initialization:读取 Settings、初始化 Included Build 和构建环境。
- Configuration:应用插件、创建项目模型、注册和配置任务、解析部分构建逻辑。
- Execution:执行任务、编译、测试、打包、上传和缓存读写。
不同阶段的优化手段不同:
| 阶段 | 常见问题 | 优先手段 |
|---|---|---|
| Initialization | Settings、插件解析或 Included Build 很慢 | 减少复杂脚本、优化插件仓库和构建逻辑 |
| Configuration | eager configuration、跨项目访问、脚本重复执行 | register、Convention Plugin、Configuration Cache |
| Execution | 编译、测试、代码生成或打包耗时 | 增量构建、Build Cache、并行和 Worker API |
| I/O | 依赖下载、日志、磁盘和远程缓存慢 | 仓库镜像、缓存、减少无关输入和输出 |
先确定瓶颈阶段,再选择优化手段。直接增大堆内存通常不能解决配置阶段脚本过慢。
24.7 Gradle Daemon 的使用与诊断
Daemon 复用 JVM、类加载器和构建服务,适合开发机重复构建:
.\gradlew.bat --status.\gradlew.bat --stop.\gradlew.bat help --no-daemon常见配置:
org.gradle.daemon=trueDaemon 可能因为以下原因重新启动或无法复用:
- Gradle 版本、Java 版本或 JVM 参数不同。
- Daemon JVM 与 Toolchain JVM 不是同一进程,不能混为一谈。
- 内存不足、进程被系统回收或构建长期空闲。
- 构建参数、环境和安全策略不允许复用。
开发机通常保留 Daemon;CI 是否复用则取决于执行器生命周期、隔离要求和内存回收策略。不要把 --stop 放在每次构建前,否则会失去 Daemon 的主要收益。
24.8 JVM 内存与垃圾回收
可以通过 gradle.properties 设置 Gradle Daemon JVM 参数:
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -Dfile.encoding=UTF-8调整内存前先确认:
- 是 Gradle 配置进程、编译器、测试进程还是外部工具占用内存。
- 是否存在频繁 Full GC、Metaspace 不足或进程被操作系统杀死。
max-workers、并行项目和测试 fork 是否同时放大内存消耗。- CI 执行器实际可用内存是否小于本地开发机。
不要仅因为“构建慢”就盲目增加 -Xmx。堆过大可能增加 GC 代价,也可能让多个并发 Worker 争抢系统内存。
24.9 并行构建的前提
启用并行项目执行:
org.gradle.parallel=true并行构建要求任务之间的关系完整、输入输出隔离。常见风险:
- 多个任务写入同一个未声明临时文件。
- 多个项目共享可变全局单例。
- 任务依赖当前工作目录而不是声明的输入。
- 通过
doLast或afterEvaluate隐式建立顺序。 - 测试使用固定端口、共享数据库或共享临时目录。
并行执行前应先确保单线程构建稳定,再通过 TestKit、集成测试和 CI 验证并行结果与串行结果一致。
24.10 Worker API 与可扩展并发
当一个任务需要处理大量相互独立的工作项时,可以使用 Worker API:
abstract class HashWorkAction : WorkAction<HashWorkParameters> { override fun execute() { val input = parameters.inputFile.asFile.get() val output = parameters.outputFile.asFile.get() output.parentFile.mkdirs() output.writeText(input.readText().hashCode().toString()) }}
interface HashWorkParameters : WorkParameters { val inputFile: RegularFileProperty val outputFile: RegularFileProperty}使用 Worker API 时要注意:
- 每个 WorkAction 的输入和输出必须独立、可追踪。
- 并发度受
org.gradle.workers.max和执行器资源限制。 - 不要在 Worker 中共享不可变性不明确的全局对象。
- 外部服务、数据库和文件写入需要独立的并发控制。
- 并行拆分的调度成本不能超过实际工作收益。
Worker API 适合隔离的 CPU 或 I/O 工作单元,不是把任意任务简单并发化的工具。
24.11 任务配置规避
配置阶段常见性能问题来自不必要的 Task 实例化和模型访问:
val generateReport = tasks.register("generateReport")
tasks.withType<Test>().configureEach { useJUnitPlatform()}
tasks.named("check") { dependsOn(generateReport)}避免:
// 不推荐val report = tasks.create("generateReport")tasks.getByName("test")tasks.all { println(name) }大型工程还应避免在根脚本中遍历所有项目并读取每个项目的依赖、文件和任务模型。把共享配置移入 Convention Plugin,并让插件使用 withPlugin、named 和 configureEach。
24.12 缓存对性能的影响
增量构建、Build Cache 和 Configuration Cache 对应不同优化边界:
- 增量构建:避免在当前输出仍有效时重复执行任务。
- Build Cache:复用其他构建已经执行的任务输出。
- Configuration Cache:复用 Settings、项目配置和任务图。
评估缓存时分别记录:
.\gradlew.bat assemble --info.\gradlew.bat assemble --configuration-cache.\gradlew.bat assemble --build-cache.\gradlew.bat assemble --scan缓存不是免费收益。缓存请求、输出上传、下载、解压和校验都有成本。优先缓存执行时间长、输出稳定、输入完整且适合跨机器复用的任务。
24.13 依赖与仓库性能
依赖解析慢时检查:
- 是否声明了过多仓库或重复仓库。
- 是否缺少内容过滤,导致每个仓库都搜索大量坐标。
- 是否频繁使用
--refresh-dependencies。 - 是否依赖不稳定的 snapshot 或 changing module。
- 远程仓库、代理、TLS 和凭据服务是否响应缓慢。
- 依赖树是否过大,平台和约束是否造成不必要的解析复杂度。
改进方式:
- 在 Settings 中集中仓库声明。
- 使用内容过滤和 exclusive content。
- 固定 release 版本并使用锁文件。
- 配置可靠的企业代理或仓库镜像。
- 不把
mavenLocal()作为所有构建的默认仓库。 - 通过
dependencyInsight处理重复和冲突依赖。
24.14 编译、测试与代码生成性能
编译和测试通常是执行阶段的主要耗时来源。可以检查:
- Java/Kotlin 编译是否获得增量输入。
- 代码生成任务是否声明输入输出并避免全量重生成。
- 测试是否被错误配置为每次启动完整外部服务。
- 是否存在重复的测试套件、重复的依赖解析或重复打包。
maxParallelForks是否超过执行器 CPU 和内存能力。- 测试报告、日志和临时文件是否产生过多 I/O。
性能优化必须保持测试隔离和结果可靠。不能为了缩短时间而跳过关键测试、关闭失败重试之外的正确性检查,或让多个测试共享未清理的数据。
24.15 性能报告与 Build Scan
常用诊断命令:
.\gradlew.bat build --profile.\gradlew.bat build --scan.\gradlew.bat build --info.\gradlew.bat build --debug.\gradlew.bat build --dry-run报告作用:
--profile:生成本地构建性能报告,适合初步比较。--scan:提供任务、配置、依赖、缓存和环境的综合视图,需遵守组织数据上传策略。--info:查看任务跳过、缓存、依赖和配置过程的常规信息。--debug:输出大量底层日志,只在定位具体问题时使用。--dry-run:查看任务图,不执行任务,适合发现任务选择和依赖关系问题。
性能报告是证据,不是结论。需要结合代码变更、构建参数和运行环境解释耗时变化。
24.16 性能基准的设计
比较优化前后的构建时间时,应区分不同场景:
| 场景 | 目的 |
|---|---|
| 冷启动构建 | 观察首次 Daemon、依赖和配置成本 |
| 温启动构建 | 观察 Daemon 复用后的实际开发体验 |
| 增量构建 | 观察小范围源码变化的反馈速度 |
| 缓存命中构建 | 观察 Build Cache 的复用收益 |
| 配置缓存命中 | 观察大型项目配置阶段收益 |
| 完整重建 | 观察从无输出开始的最差路径 |
基准流程:
- 固定 Wrapper、JDK、操作系统、CPU、内存和仓库。
- 预热 Daemon,并执行多次取稳定结果。
- 清晰记录是否使用 Build Cache 和 Configuration Cache。
- 记录任务执行时间、配置时间、缓存命中率和失败次数。
- 至少重复多次,避免单次运行受到网络和系统噪声影响。
- 比较同一场景,而不是用
clean build和增量构建直接对比。
24.17 CI 性能策略
CI 中可以组合使用:
org.gradle.caching=trueorg.gradle.parallel=trueorg.gradle.configuration-cache=true但应根据执行器资源和安全策略逐项启用。CI 性能策略还应包括:
- 缓存 Gradle User Home 中稳定的依赖和 Wrapper 内容。
- 使用远程 Build Cache 复用可信任务输出。
- 固定 JDK、工具链、插件和仓库镜像。
- 避免每个步骤都运行
clean。 - 将静态检查、测试和发布任务拆成可并行且边界清晰的作业。
- 保存失败构建的性能报告和缓存诊断日志。
CI 缓存目录和 Gradle Build Cache 不是同一件事。前者缓存文件系统内容,后者根据任务输入输出复用任务结果,配置和失效策略应分别设计。
24.18 性能优化反模式
盲目增加堆内存:没有证明内存是瓶颈,就会掩盖配置、依赖或 I/O 问题。
默认 clean build:破坏增量和缓存收益,无法代表日常开发路径。
无条件启用所有并行:任务存在共享状态时会产生竞态,测试结果可能不稳定。
把所有任务都放进 Build Cache:不确定任务缓存会产生错误结果和安全风险。
使用 --refresh-dependencies 解决所有依赖问题:会增加网络和解析成本,不能替代仓库治理。
在根脚本中读取所有项目模型:扩大配置阶段工作量并破坏项目隔离。
只看单次 --profile 结果:系统噪声、Daemon 状态和网络波动都会影响单次数据。
24.19 性能问题排错流程
- 明确是配置阶段、任务执行、依赖解析、缓存还是外部服务慢。
- 使用
--profile、--scan、--info和--dry-run获取证据。 - 比较 Daemon、并行、缓存和配置缓存开关的差异。
- 检查任务是否重复执行、是否缺少输入输出声明。
- 检查根脚本、插件和多项目跨项目访问。
- 检查仓库、网络、依赖树和缓存服务。
- 一次只改变一个变量,再重复基准测试。
- 保留优化前后报告和回滚方式。
24.20 性能配置综合示例
gradle.properties:
org.gradle.daemon=trueorg.gradle.parallel=trueorg.gradle.caching=trueorg.gradle.configuration-cache=trueorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8settings.gradle.kts:
buildCache { local { isEnabled = true }}验证命令:
.\gradlew.bat :app:build --profile.\gradlew.bat :app:build --configuration-cache --build-cache --info.\gradlew.bat :app:build --scan.\gradlew.bat :app:build --dry-run实际项目中不要直接复制所有性能参数。应根据执行器资源、任务模型、缓存服务和基准结果逐项启用。
25. 测试、覆盖率与代码质量
25.1 JUnit Jupiter
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher")}
tasks.withType<Test>().configureEach { useJUnitPlatform()}运行:
.\gradlew.bat test25.2 测试过滤
.\gradlew.bat test --tests "com.example.CalculatorTest".\gradlew.bat test --tests "com.example.*"构建脚本中配置失败策略:
tasks.withType<Test>().configureEach { failFast = false maxParallelForks = 2}并行测试必须保证测试之间没有共享可变状态。
25.3 测试报告
标准报告通常位于:
build/reports/tests/test/index.htmlCI 应保存 XML 或 HTML 报告,失败时便于定位具体测试和堆栈。
25.4 JaCoCo
plugins { jacoco}
tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true) html.required.set(true) }}覆盖率是风险信号,不是代码质量的唯一指标。应结合关键路径、变更范围和测试有效性。
25.5 Checkstyle、Spotless 等
代码质量工具通过插件引入。建议把规则文件、插件版本和失败策略纳入版本控制:
plugins { checkstyle}
checkstyle { toolVersion = "13.1.0"}不要在每个模块中复制不同版本的质量插件配置,应通过 Convention Plugin 统一。
25.6 验证生命周期
tasks.named("check") { dependsOn("jacocoTestReport")}只有当报告生成稳定、耗时可接受并且团队确实需要时,才把它接入默认 check。否则可以在 CI 的独立阶段执行。
25.7 测试任务与 Source Set
Java 插件默认提供 test Source Set 和 test 任务。复杂项目可以使用额外 Source Set 区分单元测试、集成测试和端到端测试:
src/test/java/src/integrationTest/java/src/e2eTest/java/不同测试套件应分别定义源码、资源、Configuration、测试框架、报告、超时、并行策略和是否参与 check。单元测试应快速稳定;集成测试和端到端测试应有明确的环境前置条件。
25.8 JUnit Platform 与测试框架
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher")}
tasks.withType<Test>().configureEach { useJUnitPlatform()}TestNG 或其他测试平台也应在任务中明确配置。升级测试框架后,应同时检查 Java 版本、测试发现规则、扩展、报告格式以及 IDE/CI 行为。
25.9 测试日志、过滤与 Dry Run
.\gradlew.bat test --tests "com.example.CalculatorTest".\gradlew.bat test --tests "com.example.CalculatorTest.addsTwoNumbers".\gradlew.bat test --tests "com.example.*".\gradlew.bat test --test-dry-runtasks.withType<Test>().configureEach { testLogging { events("passed", "skipped", "failed") exceptionFormat = TestExceptionFormat.FULL showStandardStreams = false }}--tests 只影响当前命令的筛选范围,--test-dry-run 用于验证测试发现但不执行测试体。调试时优先缩小范围,不要反复执行整个测试套件。
25.10 测试并行与隔离
tasks.withType<Test>().configureEach { maxParallelForks = 2 forkEvery = 100}并行测试必须避免共享可变静态状态、固定冲突端口和共享临时目录,并保证数据库、文件和消息队列资源可以并行清理。提高 maxParallelForks 不一定更快,应结合 CPU、内存、测试类型和外部服务承载能力基准测试。
25.11 JVM Test Suite
多套 JVM 测试可以使用声明式测试套件模型:
plugins { java `jvm-test-suite`}
testing { suites { val test by getting(JvmTestSuite::class) { useJUnitJupiter() } register<JvmTestSuite>("integrationTest") { useJUnitJupiter() dependencies { implementation(project()) } targets.all { testTask.configure { shouldRunAfter(test) } } } }}迁移时确认当前 Gradle 版本和插件支持情况,不要同时用传统 Source Set 和 JVM Test Suite 为同一套测试重复创建任务。
25.12 集成测试生命周期
val integrationTest = tasks.named<Test>("integrationTest")
tasks.named("check") { dependsOn(integrationTest)}如果集成测试依赖 Docker、数据库或外部服务,应将环境管理纳入 CI。适合默认 check 的测试应具备可自动准备、执行时间可接受、失败稳定和不依赖开发者本机状态等条件。
25.13 测试报告与报告聚合
单个测试任务通常生成:
build/test-results/test/build/reports/tests/test/index.html多项目工程可以使用测试报告聚合插件:
plugins { id("test-report-aggregation")}
dependencies { testReportAggregation(project(":core")) testReportAggregation(project(":service")) testReportAggregation(project(":app"))}CI 应同时保存 XML、单项目 HTML、聚合报告、失败测试输出以及测试使用的 JDK、Gradle 和依赖版本。聚合报告不能替代原始测试结果。
25.14 JaCoCo 报告
plugins { jacoco}
tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true) html.required.set(true) csv.required.set(false) }}运行:
.\gradlew.bat test jacocoTestReport覆盖率应帮助识别关键路径、分支和边界条件的测试缺口。覆盖率数字不能证明测试有效,也不能替代代码审查和变更风险评估。
25.15 JaCoCo 覆盖率门禁
tasks.jacocoTestCoverageVerification { dependsOn(tasks.test) violationRules { rule { limit { counter = "LINE" value = "COVEREDRATIO" minimum = "0.80".toBigDecimal() } } }}
tasks.named("check") { dependsOn(tasks.jacocoTestCoverageVerification)}建议先建立基线,再逐步提高阈值;对生成代码、适配层和测试夹具设置有记录的排除规则;将覆盖率下降与变更范围结合,而不是只看全局平均值。
25.16 多项目覆盖率聚合
plugins { id("jacoco-report-aggregation")}
dependencies { jacocoAggregation(project(":core")) jacocoAggregation(project(":service")) jacocoAggregation(project(":app"))}聚合时确认每个项目的测试和 execution data 已生成,源码、类文件和报告路径能够匹配,排除规则一致,并且总覆盖率不会掩盖关键模块的低覆盖率。
25.17 Checkstyle 与质量规则
plugins { checkstyle}
checkstyle { toolVersion = "13.1.0" configFile = rootProject.file("config/checkstyle/checkstyle.xml")}
tasks.withType<Checkstyle>().configureEach { reports { xml.required.set(true) html.required.set(true) }}规则文件、工具版本和抑制列表应纳入版本控制。质量工具升级时检查 Java 语法支持、报告格式、规则变化和执行耗时。
25.18 Convention Plugin 与质量门禁
多个模块不要复制质量插件配置,应通过 Convention Plugin 统一:
plugins { checkstyle jacoco}
tasks.withType<Checkstyle>().configureEach { maxWarnings = 0}
tasks.withType<Test>().configureEach { useJUnitPlatform()}模块只声明:
plugins { id("company.quality")}可以把 CI 验证分层:
快速门禁:编译、单元测试、基础 Checkstyle完整门禁:集成测试、JaCoCo、静态分析、依赖验证发布门禁:完整检查、签名、制品消费测试和安全扫描CI 应保存报告并返回明确退出码,不应只打印“覆盖率不足”却继续发布。
25.19 测试与质量综合示例
plugins { java jacoco checkstyle}
repositories { mavenCentral()}
dependencies { testImplementation("org.junit.jupiter:junit-jupiter:6.0.3") testRuntimeOnly("org.junit.platform:junit-platform-launcher")}
tasks.withType<Test>().configureEach { useJUnitPlatform() testLogging { events("passed", "skipped", "failed") exceptionFormat = TestExceptionFormat.FULL }}
tasks.jacocoTestReport { dependsOn(tasks.test) reports { xml.required.set(true) html.required.set(true) }}
tasks.jacocoTestCoverageVerification { dependsOn(tasks.test) violationRules { rule { limit { minimum = "0.80".toBigDecimal() } } }}
checkstyle { configFile = rootProject.file("config/checkstyle/checkstyle.xml")}
tasks.named("check") { dependsOn(tasks.jacocoTestCoverageVerification)}验证命令:
.\gradlew.bat clean check.\gradlew.bat test --tests "com.example.*".\gradlew.bat jacocoTestReport.\gradlew.bat checkstyleMain checkstyleTest26. 打包与发布
构建成功并不等于可以交付。一个可用的发布流程还需要回答以下问题:
- 交付的是普通 JAR、可执行 JAR、应用分发包,还是供其他项目依赖的 Maven 模块?
- 构件名称、版本、分类器、Manifest、POM 和 Gradle Module Metadata 是否正确?
- 同一份源码在相同环境中能否生成字节级稳定的归档文件?
- Release 与 Snapshot 是否发布到正确仓库,旧版本是否保持不可变?
- 仓库凭据和签名私钥是否只来自本机或 CI Secret?
- 发布后是否通过独立消费方验证,而不是只确认上传任务成功?
本章以 JVM 项目为主,串联 JAR、Application Distribution、maven-publish、signing 与发布验证,形成一条可审计、可复现、可回滚的交付链路。
26.1 先区分打包、分发与发布
三个概念经常被混用,但它们解决的问题不同:
| 概念 | 典型任务 | 主要产物 | 适用场景 |
|---|---|---|---|
| 打包 | jar、自定义 Jar | .jar、.zip、.tar | 生成单个归档构件 |
| 分发 | installDist、distZip、distTar | 启动脚本、依赖和配置组成的目录或压缩包 | 交付可直接运行的 JVM 应用 |
| 发布 | publish、publishToMavenLocal | JAR、POM、.module、签名和校验文件 | 让其他构建通过仓库坐标消费模块 |
选择交付方式时可以遵循以下原则:
- 类库:优先发布标准 Maven 模块,不要把依赖全部塞进一个 Fat JAR。
- 命令行应用:优先使用
application插件生成分发包。 - 插件或平台:发布由对应组件生成的变体和元数据。
- 临时演示程序:可以生成可执行 JAR,但仍要明确外部依赖如何提供。
- 容器化服务:Gradle 负责生成应用构件,镜像构建应作为后续、可独立验证的阶段。
26.2 JAR 任务与默认产物
Java 插件会注册 jar 任务,并将主 Source Set 的编译输出和资源打入归档:
.\gradlew.bat clean jar默认产物通常位于:
build/libs/<project-name>-<version>.jar配置项目坐标与基础归档信息:
plugins { java}
group = "com.example"version = "1.0.0"
base { archivesName = "example-core"}
tasks.jar { archiveClassifier = ""}常用归档属性如下:
| 属性 | 示例 | 说明 |
|---|---|---|
archiveBaseName | example-core | 基础文件名 |
archiveAppendix | cli | 附加名称 |
archiveVersion | 1.0.0 | 归档版本,默认来自 project.version |
archiveClassifier | all、sources | 区分同一模块的附属构件 |
archiveExtension | jar | 文件扩展名 |
destinationDirectory | layout.buildDirectory.dir("libs") | 输出目录 |
通常优先通过 base.archivesName 和项目 version 统一命名,只在确实需要附属构件时使用 classifier,避免每个任务独立拼接文件名。
查看归档内容:
jar tf build\libs\example-core-1.0.0.jar26.3 Manifest 与可执行入口
JAR 的 META-INF/MANIFEST.MF 可以记录版本、实现名称和入口类:
tasks.jar { manifest { attributes( "Main-Class" to "com.example.cli.Main", "Implementation-Title" to project.name, "Implementation-Version" to project.version, "Implementation-Vendor" to "Example Team" ) }}执行可运行 JAR:
java -jar build\libs\example-core-1.0.0.jar需要注意:
Main-Class只声明入口,不会自动把运行时依赖复制进 JAR。- 不要默认写入当前时间、绝对路径、用户名等易变化信息,否则会破坏可复现性。
Class-PathManifest 属性只适合依赖布局固定的简单场景;普通应用更适合使用分发包。- Manifest 元数据应来自项目模型或版本管理,而不是在多个任务中重复硬编码。
检查 Manifest:
jar xf build\libs\example-core-1.0.0.jar META-INF\MANIFEST.MFGet-Content META-INF\MANIFEST.MF26.4 可复现归档
可复现归档指在输入不变时,重复构建得到内容顺序、时间戳和字节结果稳定的产物。它有利于缓存命中、供应链审计、签名验证和构建结果比对。
可以显式统一所有归档任务:
import org.gradle.api.tasks.bundling.AbstractArchiveTask
tasks.withType<AbstractArchiveTask>().configureEach { isPreserveFileTimestamps = false isReproducibleFileOrder = true}还应避免以下非确定性输入:
- 在 Manifest 或生成资源中写入
Instant.now()。 - 把工作区绝对路径、临时目录或操作系统用户名写入产物。
- 依赖未锁定的动态版本或变化模块。
- 使用未声明输入输出的脚本在归档前修改文件。
- 让不同 JDK、不同字符集或不同换行符生成不一致的文档。
对关键 Release,可以连续构建两次并比较哈希:
.\gradlew.bat clean jarGet-FileHash build\libs\example-core-1.0.0.jar -Algorithm SHA256
.\gradlew.bat clean jarGet-FileHash build\libs\example-core-1.0.0.jar -Algorithm SHA256哈希不一致时,应先比较归档条目、Manifest、生成代码和时间戳,而不是直接关闭缓存或忽略差异。
26.5 Sources JAR 与 Javadoc JAR
类库发布通常应同时提供源码和 API 文档构件:
java { withSourcesJar() withJavadocJar()}Java 插件会创建:
sourcesJar:打包主 Source Set 的源码。javadocJar:打包javadoc任务生成的文档。- 对应的
sourcesElements、javadocElements变体。
统一 Javadoc 编码:
import org.gradle.external.javadoc.StandardJavadocDocletOptions
tasks.withType<Javadoc>().configureEach { options.encoding = "UTF-8" (options as StandardJavadocDocletOptions).apply { charSet = "UTF-8" docEncoding = "UTF-8" }}执行与检查:
.\gradlew.bat sourcesJar javadocJarGet-ChildItem build\libs如果 Javadoc 因代码本身的文档错误而失败,优先修复文档;不建议为了发布成功而全局关闭 DocLint 或忽略任务失败。
26.6 Fat JAR、Uber JAR 与 Shadow JAR
Fat JAR 会把项目输出和运行时依赖合并进一个归档。它便于单文件分发,但也会带来明显代价:
- 多个依赖中的同名资源可能互相覆盖。
META-INF/services等服务描述文件需要合并,而不是简单去重。- 签名文件在重新打包后通常失效。
- 许可证、Notice 和依赖来源更难追踪。
- 类冲突和包重定位问题会被推迟到运行期。
- 对类库发布而言,会破坏正常的依赖图和版本冲突处理。
简单实验可以注册自定义任务:
val fatJar by tasks.registering(Jar::class) { group = "build" description = "Builds an executable JAR containing runtime dependencies." archiveClassifier = "all" duplicatesStrategy = DuplicatesStrategy.EXCLUDE
manifest { attributes["Main-Class"] = "com.example.cli.Main" }
from(sourceSets.main.get().output) dependsOn(configurations.runtimeClasspath) from({ configurations.runtimeClasspath.get().map { dependency -> if (dependency.isDirectory) dependency else zipTree(dependency) } })
exclude( "META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA" )}该示例没有解决服务文件合并、包重定位和复杂重复资源策略,只适合学习或非常简单的应用。生产项目应使用成熟的 Shadow 方案,或者优先使用 Application Distribution、容器镜像和普通依赖发布。
26.7 Application 分发包
application 插件会应用 Distribution 能力,生成启动脚本、运行时依赖和应用 JAR:
plugins { application}
application { mainClass = "com.example.cli.Main" applicationDefaultJvmArgs = listOf( "-Dfile.encoding=UTF-8", "-XX:+ExitOnOutOfMemoryError" )}常用任务:
.\gradlew.bat run.\gradlew.bat installDist.\gradlew.bat distZip.\gradlew.bat distTar典型输出:
build/install/<application-name>/build/distributions/<application-name>-<version>.zipbuild/distributions/<application-name>-<version>.tar自定义分发内容:
distributions { main { distributionBaseName = "example-cli" contents { from("README.md") from("config") { into("conf") } } }}分发包适合“解压即可运行”的交付方式。配置文件模板可以随包发布,但环境密码、Token 和私钥不得写入归档。
26.8 Maven Publish 的模型
maven-publish 插件的核心模型由三部分组成:
- Component:插件暴露的可发布软件组件,例如
components["java"]。 - Publication:某次发布包含的构件与元数据,例如
MavenPublication。 - Repository:Publication 的目标仓库。
基础配置:
plugins { `java-library` `maven-publish`}
group = "com.example"version = "1.0.0"
publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) artifactId = "example-core" } }
repositories { maven { name = "localBuildRepo" url = layout.buildDirectory.dir("repo") } }}执行:
.\gradlew.bat publish不要只用 artifact(tasks.jar) 手工发布主 JAR来替代 from(components["java"])。软件组件能够携带依赖、变体、sources、javadoc 和 Gradle Module Metadata 等更完整的信息。
26.9 Publication 坐标与附属构件
Maven 坐标由 groupId、artifactId 和 version 组成:
publishing { publications { named<MavenPublication>("mavenJava") { groupId = "com.example.libs" artifactId = "example-core" version = project.version.toString() } }}一般建议:
groupId使用组织拥有的反向域名。artifactId稳定表达模块职责,不把临时实现细节放进名称。version遵循项目统一的版本策略。- 同一坐标的 Release 一经公开,不应覆盖。
- classifier 只用于附属构件,不用于模拟新的独立模块。
添加额外构件:
val schemaZip by tasks.registering(Zip::class) { archiveClassifier = "schema" from("src/main/schema")}
publishing { publications { named<MavenPublication>("mavenJava") { artifact(schemaZip) } }}消费方只有明确知道 classifier 时才能使用附属构件。如果该内容有独立依赖关系或生命周期,更适合拆成单独模块或建模为 Gradle 变体。
26.10 完整 POM 元数据
公共仓库通常要求完整的项目、许可证、开发者和 SCM 信息。即使内部仓库不强制,也应提供足够信息支持追踪与审计:
publishing { publications { named<MavenPublication>("mavenJava") { pom { name = "Example Core" description = "Reusable core APIs for Example applications." url = "https://example.com/example-core"
licenses { license { name = "The Apache License, Version 2.0" url = "https://www.apache.org/licenses/LICENSE-2.0.txt" distribution = "repo" } }
developers { developer { id = "example-team" name = "Example Team" email = "dev@example.com" } }
scm { connection = "scm:git:https://example.com/example/example-core.git" developerConnection = "scm:git:ssh://git@example.com/example/example-core.git" url = "https://example.com/example/example-core" tag = "HEAD" }
issueManagement { system = "GitHub Issues" url = "https://example.com/example/example-core/issues" } } } }}POM 中的 URL、邮箱和许可证必须替换为真实信息。不要为了通过仓库校验而填写不可访问的占位地址。
生成并检查 POM:
.\gradlew.bat generatePomFileForMavenJavaPublicationGet-Content build\publications\mavenJava\pom-default.xml26.11 依赖作用域与发布元数据
java-library 插件能够把依赖意图映射到 Maven 作用域:
| Gradle 声明 | 对类库消费者的含义 | 常见 POM 结果 |
|---|---|---|
api | 暴露在公共 API,消费者编译期需要 | compile |
implementation | 实现细节,消费者运行期需要 | runtime |
compileOnly | 仅当前模块编译需要 | 通常不作为普通运行依赖发布 |
runtimeOnly | 仅运行期需要 | runtime |
testImplementation | 仅测试使用 | 不发布 |
示例:
dependencies { api("org.slf4j:slf4j-api:2.0.17") implementation("com.fasterxml.jackson.core:jackson-databind:2.19.2") runtimeOnly("ch.qos.logback:logback-classic:1.5.18")}错误地把所有依赖声明为 api 会扩大消费者编译类路径、增加耦合并泄漏实现细节。发布前应结合 dependencies、dependencyInsight 和实际公共 API 检查作用域。
26.12 版本映射与解析后版本
默认 POM 通常记录构建脚本声明的版本。如果项目使用版本约束、平台、富版本或动态版本,可能需要把解析后的版本写入 POM,以便 Maven 消费方获得更接近 Gradle 解析结果的依赖版本:
publishing { publications { named<MavenPublication>("mavenJava") { versionMapping { usage("java-api") { fromResolutionOf("runtimeClasspath") } usage("java-runtime") { fromResolutionResult() } } } }}使用解析后版本前要明确其语义:
- 它会把当前仓库状态和依赖解析结果固化到发布元数据。
- 发布构建应启用依赖锁定或其他稳定版本策略。
- 不应在 Release 构建中依赖
latest.release、版本区间或变化模块。 - 平台和约束应优先作为依赖模型的一部分发布,而不是靠脚本修改 POM。
26.13 Gradle Module Metadata
发布 Java 组件时,Gradle 除了生成 Maven POM,还会生成 Gradle Module Metadata,扩展名为 .module。它能够表达 Maven POM 难以完整描述的信息:
- API 与 Runtime 等多个变体。
- 属性、能力和 Feature Variant。
- 严格版本、首选版本和拒绝版本。
- 依赖约束、平台关系和丰富版本语义。
- sources、javadoc 等附属变体。
生成元数据:
.\gradlew.bat generateMetadataFileForMavenJavaPublicationGet-Content build\publications\mavenJava\module.json通常应同时发布 POM 和 .module:
- Maven 消费方读取 POM。
- Gradle 消费方优先利用更丰富的 Module Metadata。
- 不要无故禁用元数据警告;警告通常表示 POM 无法表达完整组件模型。
- 如果确实需要抑制特定警告,应先确认 Maven 消费方不会获得错误依赖信息。
26.14 发布仓库与本地演练
在连接远程仓库前,先发布到 build 下的临时 Maven 仓库:
publishing { repositories { maven { name = "stagingDirectory" url = layout.buildDirectory.dir("staging-repo") } }}执行指定仓库任务:
.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository本地目录仓库便于检查:
- 坐标目录是否符合预期。
- 主 JAR、sources、javadoc 是否齐全。
- POM、
.module、签名和校验文件是否存在。 - 独立示例项目能否只通过仓库坐标解析并运行。
也可以发布到 Maven Local:
.\gradlew.bat publishToMavenLocalpublishToMavenLocal 适合临时兼容 Maven 工具,但不应成为团队发布验证的唯一方式,因为 ~/.m2/repository 容易残留旧构件、覆盖相同坐标并掩盖仓库配置问题。可清理的项目内临时仓库更适合自动化验证。
26.15 Release 与 Snapshot 仓库路由
很多仓库分别管理快照和正式版本。可以根据版本后缀选择目标:
val isSnapshot = version.toString().endsWith("-SNAPSHOT")
publishing { repositories { maven { name = "internal" url = uri( if (isSnapshot) { "https://repo.example.com/maven-snapshots" } else { "https://repo.example.com/maven-releases" } ) } }}建议的版本规则:
- Snapshot 使用明确后缀,例如
1.4.0-SNAPSHOT。 - Release 使用不可变版本,例如
1.4.0。 - 发布 Release 前确认 Git 工作区干净、Tag 与版本一致。
- CI 中限制 Release 发布只能由受保护 Tag 或人工审批触发。
- 禁止覆盖已存在的 Release;修复问题应发布新版本。
- 若仓库支持 Staging,应先关闭并验证 Staging Repository,再执行正式 Release。
26.16 凭据与 Secret 管理
仓库用户名、密码、Token 和私钥不得写入 build.gradle.kts、gradle.properties 模板或 Git 历史。
仓库可以声明密码凭据:
import org.gradle.api.credentials.PasswordCredentials
publishing { repositories { maven { name = "internal" url = uri("https://repo.example.com/maven-releases") credentials(PasswordCredentials::class) } }}Gradle 可以根据仓库名称读取属性:
internalUsername=your-userinternalPassword=your-token这些值只应放在用户级 ~/.gradle/gradle.properties 或 CI Secret 中,不应提交到仓库。CI 也可以通过环境变量映射为 Gradle 属性:
$env:ORG_GRADLE_PROJECT_internalUsername = $env:MAVEN_REPO_USERNAME$env:ORG_GRADLE_PROJECT_internalPassword = $env:MAVEN_REPO_PASSWORD.\gradlew.bat publish安全原则:
- 为发布机器人使用最小权限、可轮换的专用 Token。
- Snapshot 与 Release 可以使用不同凭据。
- 避免在
--info、--debug、异常消息或自定义日志中打印 Secret。 - Fork Pull Request 不应获得 Release Secret。
- 只在真正执行远程发布任务时要求凭据,普通
build不应因此失败。
26.17 构件签名
公共仓库或组织策略可能要求使用 PGP 对发布构件签名。Gradle 的 signing 插件会为 Publication 创建签名任务:
plugins { `java-library` `maven-publish` signing}
signing { val signingKey = providers.environmentVariable("SIGNING_KEY") val signingPassword = providers.environmentVariable("SIGNING_PASSWORD")
if (signingKey.isPresent) { useInMemoryPgpKeys(signingKey.get(), signingPassword.orNull) }
isRequired = !version.toString().endsWith("-SNAPSHOT") sign(publishing.publications["mavenJava"])}CI 中的 SIGNING_KEY 通常保存 ASCII-armored 私钥文本,SIGNING_PASSWORD 保存私钥口令。也可以使用用户级 Gradle 属性和本机密钥环,但都不得提交到项目仓库。
签名策略应明确:
- Release 必须签名,缺少密钥时立即失败。
- 本地 Snapshot 可以按团队策略跳过签名。
- 签名证明的是构件与签名者的关系,不替代仓库权限控制、哈希校验和来源证明。
- 签名前必须完成全部构件生成;签名后不得再修改归档内容。
- 私钥应设置有效期、备份、轮换和吊销流程。
26.18 发布任务图与常用命令
每个 Publication 和 Repository 的组合都会生成专用任务。以 mavenJava Publication 和 internal Repository 为例,常见任务包括:
generatePomFileForMavenJavaPublicationgenerateMetadataFileForMavenJavaPublicationsignMavenJavaPublicationpublishMavenJavaPublicationToInternalRepositorypublishMavenJavaPublicationToMavenLocalpublishpublishToMavenLocal查看发布任务:
.\gradlew.bat tasks --group publishing.\gradlew.bat tasks --group signing查看执行计划:
.\gradlew.bat publishMavenJavaPublicationToInternalRepository --dry-run发布任务命名由 Publication 名称和 Repository 名称组成。稳定、语义明确的名称有利于 CI 精确调用和权限控制。
26.19 发布前后验证
远程上传成功只说明仓库接收了文件,不代表模块可正确消费。建议建立以下验证链路。
发布前:
.\gradlew.bat clean check.\gradlew.bat jar sourcesJar javadocJar.\gradlew.bat generatePomFileForMavenJavaPublication.\gradlew.bat generateMetadataFileForMavenJavaPublication.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository检查内容:
- 测试、静态分析和覆盖率门禁全部通过。
- 版本、Tag、坐标和仓库类型一致。
- JAR 中没有测试类、临时文件、Secret 或重复资源。
- POM 许可证、SCM、开发者和依赖作用域正确。
.module中的变体、能力和依赖约束符合设计。- sources 和 javadoc 可以解压并正常查看。
- Release 的签名与校验文件完整。
发布后:
- 创建独立 Consumer 项目,只声明发布仓库和模块坐标。
- 清理本地缓存或使用全新 Gradle User Home。
- 分别验证 Gradle 和 Maven 消费方式。
- 编译并运行最小调用示例。
- 检查依赖图、变体选择和传递依赖。
- 记录仓库中的最终地址、构件哈希、CI Run 和 Git Commit。
独立消费验证能够发现发布工程自身类路径掩盖的问题,例如漏发运行时依赖、POM 作用域错误、仓库内容不完整和错误的 Java 版本属性。
26.20 多项目发布与 Convention Plugin
多项目构建不应默认把所有子项目都发布。可以将模块划分为:
- 可发布类库。
- 只供内部组合的实现模块。
- 应用模块。
- 测试夹具或基准模块。
- Platform/BOM 模块。
推荐把统一发布规则放入 Convention Plugin,例如:
build-logic/└── src/main/kotlin/ └── company.java-publish.gradle.kts约定插件可以集中管理:
group、Java Toolchain 和归档可复现性。- sources、javadoc 和 Publication 创建。
- POM 中的组织、许可证和 SCM 公共字段。
- Snapshot/Release 仓库路由。
- 凭据名称、签名策略和发布检查。
- Release 任务仅在 CI 或受保护分支启用。
各子项目只保留差异信息:
plugins { id("company.java-publish")}
publishing { publications { named<MavenPublication>("mavenJava") { artifactId = "example-json" pom { name = "Example JSON" description = "JSON integration for the Example platform." } } }}相比根脚本中的大段 subprojects {},Convention Plugin 具有清晰的输入边界、可测试性和按需应用能力,也更容易逐步迁移。
26.21 完整类库发布示例
下面的示例组合 Java Library、sources、javadoc、POM、本地演练仓库、远程仓库和签名。真实项目应替换坐标与 URL:
import org.gradle.api.credentials.PasswordCredentialsimport org.gradle.api.tasks.bundling.AbstractArchiveTaskimport org.gradle.external.javadoc.StandardJavadocDocletOptions
plugins { `java-library` `maven-publish` signing}
group = "com.example"version = providers.gradleProperty("releaseVersion").orElse("1.0.0-SNAPSHOT").get()
base { archivesName = "example-core"}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } withSourcesJar() withJavadocJar()}
tasks.withType<AbstractArchiveTask>().configureEach { isPreserveFileTimestamps = false isReproducibleFileOrder = true}
tasks.withType<Javadoc>().configureEach { options.encoding = "UTF-8" (options as StandardJavadocDocletOptions).apply { charSet = "UTF-8" docEncoding = "UTF-8" }}
publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) artifactId = "example-core"
pom { name = "Example Core" description = "Core APIs for the Example platform." url = "https://example.com/example-core"
licenses { license { name = "The Apache License, Version 2.0" url = "https://www.apache.org/licenses/LICENSE-2.0.txt" distribution = "repo" } }
developers { developer { id = "example-team" name = "Example Team" } }
scm { connection = "scm:git:https://example.com/example/example-core.git" developerConnection = "scm:git:ssh://git@example.com/example/example-core.git" url = "https://example.com/example/example-core" } } } }
repositories { maven { name = "stagingDirectory" url = layout.buildDirectory.dir("staging-repo") }
maven { name = "internal" val snapshotsUrl = "https://repo.example.com/maven-snapshots" val releasesUrl = "https://repo.example.com/maven-releases" url = uri( if (version.toString().endsWith("-SNAPSHOT")) snapshotsUrl else releasesUrl ) credentials(PasswordCredentials::class) } }}
signing { val signingKey = providers.environmentVariable("SIGNING_KEY") val signingPassword = providers.environmentVariable("SIGNING_PASSWORD")
if (signingKey.isPresent) { useInMemoryPgpKeys(signingKey.get(), signingPassword.orNull) }
isRequired = !version.toString().endsWith("-SNAPSHOT") sign(publishing.publications["mavenJava"])}推荐执行顺序:
.\gradlew.bat clean check.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository.\gradlew.bat publishMavenJavaPublicationToInternalRepository --dry-run.\gradlew.bat publishMavenJavaPublicationToInternalRepository远程发布命令应只在拥有 Secret 的受保护 CI 环境执行。本地开发者默认只需运行 build 或发布到项目内临时仓库。
26.22 常见问题与排查
问题一:java -jar 提示找不到依赖类
原因通常是普通 JAR 不包含运行时依赖。使用 Application Distribution、正确的运行时类路径,或经过验证的 Shadow 构建,不要误以为设置 Main-Class 就会生成 Fat JAR。
问题二:POM 中没有依赖或作用域错误
确认 Publication 使用 from(components["java"]),并检查 api、implementation、runtimeOnly 的声明。不要只发布手工添加的 JAR。
问题三:publish 要求凭据,但只想发布到本地目录
将本地和远程仓库分开命名,并执行精确任务,例如:
.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository同时避免在配置阶段无条件读取远程 Secret。
问题四:签名任务找不到 Signatory
检查私钥是否为完整 ASCII-armored 文本、换行是否被 CI Secret 保留、口令是否匹配,以及 Release 是否按策略要求签名。
问题五:仓库已有相同 Release
不要覆盖。提升版本、修复发布流程并重新发布。相同坐标对应不同内容会破坏缓存、依赖验证和供应链审计。
问题六:Gradle 消费正常,Maven 消费失败
Gradle 可能使用了 .module 中更丰富的元数据,而 Maven 只能读取 POM。检查 POM 依赖作用域、可选依赖、BOM 导入和 classifier,不要只测试 Gradle Consumer。
问题七:归档每次构建哈希不同
检查 Manifest 时间、生成资源、文件顺序、保留时间戳、Javadoc 输出、JDK 版本和外部脚本。先定位非确定性输入,再调整任务声明。
26.23 发布检查清单
坐标与版本:
-
groupId、artifactId、version与发布计划一致。 - Snapshot 与 Release 使用正确仓库。
- Release 坐标未被使用,版本不可变。
- Git Commit、Tag 与版本可以互相追踪。
构件与元数据:
- 主 JAR、sources JAR、javadoc JAR 完整。
- Manifest 不包含非确定性或敏感信息。
- POM 项目说明、许可证、开发者和 SCM 正确。
- POM 依赖作用域符合
api/implementation设计。 - Gradle Module Metadata 变体和约束正确。
- 归档可复现,关键产物已记录 SHA-256。
安全与流程:
- 凭据和私钥来自用户级配置或 CI Secret。
- 发布账号遵循最小权限并可以轮换。
- Release 构件已签名且签名可验证。
- 发布任务只由受保护分支、Tag 或审批触发。
- 独立 Consumer 已验证编译、运行和依赖图。
- 发布记录包含 CI Run、Commit、坐标和哈希。
27. 依赖锁定与供应链安全
依赖管理不仅是“把库下载下来”。构建真正需要保证的是:依赖来源可信、解析结果可复现、升级过程可审查、构建行为可追踪,并且在出现安全事件时能够快速定位和处置。
本章中的“依赖”包括:
- 项目在
dependencies {}中声明的库和插件。 - 传递依赖、平台、BOM、版本约束和 Feature Variant。
- Gradle Plugin Portal、Maven Central、内部仓库中的构件和元数据。
- Gradle Wrapper、构建逻辑、Convention Plugin 和 CI 使用的工具。
- 构建脚本下载或执行的外部程序。
一个成熟的依赖治理流程应同时使用以下机制:
- 使用版本目录、平台和约束统一版本意图。
- 使用依赖锁定固定具体解析结果。
- 使用依赖验证确认下载文件未被替换。
- 使用仓库集中声明和内容过滤控制来源。
- 使用 SBOM 和漏洞扫描了解交付软件组成。
- 使用 CI 审计、审批和变更记录控制升级风险。
27.1 依赖解析与供应链威胁
Gradle 解析依赖时,不只是读取一行坐标,还会处理版本选择、传递依赖、变体、属性、能力、仓库优先级和元数据。相同的直接依赖声明,在仓库内容、元数据或约束发生变化时,可能得到不同结果。
常见风险包括:
- 动态版本在不同日期解析到不同版本。
- 版本区间被新发布的版本自动满足。
- 传递依赖悄然升级并引入行为变化。
- 同一坐标在不同仓库返回不同文件。
- 本地缓存掩盖了远程仓库或校验配置的问题。
- 未验证的构件被代理仓库、镜像或网络链路替换。
- 构建脚本、插件或初始化脚本执行了未审查的代码。
- 依赖升级只修改版本号,没有审查许可证、API、运行时行为和漏洞影响。
因此,“声明了固定版本”与“构建结果可复现”不是同一个概念。固定版本可以减少不确定性,但仍然需要锁定解析结果和验证构件内容。
27.2 固定版本、版本目录与平台
最基础的版本治理是避免在多个模块中散落版本字符串:
dependencies { implementation("org.apache.commons:commons-lang3:3.18.0") testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")}更适合多模块项目的方式是使用 Version Catalog:
[versions]commonsLang = "3.18.0"junit = "5.13.3"
[libraries]commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang" }junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }dependencies { implementation(libs.commons.lang3) testImplementation(libs.junit.jupiter)}Version Catalog 解决的是“版本声明集中、别名统一和依赖引用可读”,并不会自动锁定最终解析结果,也不会自动替代漏洞扫描。
对于一组需要一起升级或保持兼容的依赖,应使用 Java Platform 或 BOM:
plugins { `java-platform`}
javaPlatform { allowDependencies()}
dependencies { constraints { api("org.junit:junit-bom:5.13.3") api("com.fasterxml.jackson:jackson-bom:2.19.2") }}库项目可以通过 platform(project(":dependencies-platform")) 统一消费版本。平台表达“哪些版本应该一起使用”,锁文件表达“这次构建最终解析到了哪些版本”,二者职责不同,可以同时使用。
27.3 Dependency Locking 基础
Dependency Locking 会记录 Configuration 最终解析出的模块版本,从而避免传递依赖在没有代码变更时自行漂移。
在需要锁定的项目中启用:
configurations.configureEach { resolutionStrategy.activateDependencyLocking()}对于要发布或交付的构建,建议至少锁定以下 Configuration:
compileClasspathruntimeClasspathtestCompileClasspathtestRuntimeClasspath- 集成测试、代码质量和自定义工具对应的可解析 Configuration
生成锁文件:
.\gradlew.bat dependencies --write-locks常见锁文件位置:
gradle/dependency-locks/<configuration>.lockfile锁文件应提交到版本控制。它是构建输入的一部分,不是可以随意删除的缓存文件。
27.4 锁文件更新与审查
升级依赖时,不要直接删除全部锁文件再重新生成。更安全的方式是只更新目标模块:
.\gradlew.bat dependencies --update-locks org.example:example-core多模块项目可以指定一个或多个模块:
.\gradlew.bat dependencies --update-locks org.example:example-core,org.example:example-api推荐的更新流程:
- 在独立分支修改版本目录、平台或直接依赖声明。
- 使用
--update-locks只更新预期模块。 - 对比锁文件差异,确认没有无关传递依赖变化。
- 执行完整测试、静态分析和依赖审计。
- 在变更说明中记录升级原因、兼容性影响和漏洞状态。
- 通过代码审查后合并锁文件和声明文件。
如果一次升级导致大量无关锁文件变化,应先确认仓库元数据、解析规则、仓库顺序和 Gradle 版本是否发生变化,而不是直接接受全部差异。
27.5 锁定模式与开发体验
开发环境有时需要临时探索新版本,CI 和 Release 构建则应拒绝未锁定依赖。可以按环境区分策略:
configurations.configureEach { resolutionStrategy.activateDependencyLocking()}实践中可以采用以下约定:
- 本地常规构建默认使用已提交锁文件。
- 依赖升级任务显式使用
--update-locks或--write-locks。 - CI 不允许通过未审查的
--write-locks修改工作区。 - Release 构建必须在干净工作区中执行,并检查锁文件没有未提交变更。
- 临时实验版本在独立分支进行,不要把
--offline或--refresh-dependencies当作长期策略。
检查锁定是否生效:
.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencyInsight --dependency example-core --configuration runtimeClasspath日志中应能看出最终选择的版本;如果某个 Configuration 没有锁定,应补充对应配置或解释为什么它不参与交付构建。
27.6 依赖约束与冲突治理
依赖约束表达“某个模块允许或要求的版本范围”,比在每个 implementation 上重复版本更适合治理传递依赖:
dependencies { constraints { implementation("org.example:example-core:1.8.2") { because("Fixes the parser denial-of-service issue") } testImplementation("org.assertj:assertj-core:3.27.3") { because("Keep assertions consistent across test modules") } }}约束的价值在于:
- 即使依赖是传递引入,也能表达组织的最低版本要求。
because可以记录升级原因,帮助后续审查。- 可以与平台、Version Catalog 和锁文件配合使用。
- 不需要把所有传递依赖都提升为直接依赖。
发现冲突时先定位来源:
.\gradlew.bat dependencyInsight ` --dependency jackson-databind ` --configuration runtimeClasspath不要为了“让构建通过”无条件使用最高版本策略。强制版本可能掩盖组件兼容性问题,正确做法是确认冲突来源、兼容矩阵和测试结果,再选择约束、平台或显式版本。
27.7 Rich Version 与 Resolution Strategy
Gradle 支持严格版本、首选版本、拒绝版本和版本范围等丰富版本语义:
dependencies { implementation("org.example:example-core") { version { strictly("1.8.2") because("Release is validated against this API version") } }}也可以拒绝已知不兼容版本:
dependencies { implementation("org.example:example-core") { version { prefer("1.8.2") reject("1.7.0") reject("1.7.1") } }}组件选择规则适合组织级兼容性门禁,但不应写成难以解释的黑盒脚本:
configurations.configureEach { resolutionStrategy.componentSelection { all { if (candidate.version.contains("-rc") || candidate.version.contains("-alpha")) { reject("Pre-release versions are not allowed in stable builds") } } }}规则越复杂,越需要测试、文档和迁移计划。优先使用平台、约束和版本目录表达静态治理规则,只有确有必要时才使用动态 Resolution Strategy。
27.8 集中声明仓库
仓库应尽量在 settings.gradle.kts 中集中声明,避免子项目偷偷添加未经审查的仓库:
import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement { repositories { gradlePluginPortal() mavenCentral() maven { name = "companyPluginRepository" url = uri("https://repo.example.com/gradle-plugins") } }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() maven { name = "companyRepository" url = uri("https://repo.example.com/maven") } }}集中仓库的好处:
- 所有项目使用一致的仓库列表和顺序。
- 新增仓库可以通过一次代码审查完成。
- 可以禁止子项目修改仓库配置。
- CI 能更容易审计访问地址和网络权限。
- 可以配合仓库内容过滤减少误解析和名称抢注风险。
仓库顺序不是安全边界。对于组织私有坐标,应使用内容过滤或独占内容,而不是仅依赖仓库排列顺序。
27.9 仓库内容过滤与独占内容
如果一个仓库只提供组织坐标,可以显式限制它允许解析的 group:
dependencyResolutionManagement { repositories { mavenCentral() exclusiveContent { forRepository { maven { name = "companyRepository" url = uri("https://repo.example.com/maven") } } filter { includeGroupByRegex("com\\.example(\\..*)?") } } }}普通内容过滤示例:
maven { url = uri("https://repo.example.com/maven") content { includeGroup("com.example.internal") includeGroupByRegex("com\\.example\\..*") }}内容过滤可以减少错误来源,但不能替代依赖验证。仍然需要锁定坐标、验证文件哈希或签名,并对仓库管理员、代理缓存和访问权限进行治理。
27.10 Dependency Verification 基础
Dependency Verification 用于检查依赖文件和元数据是否与项目记录的校验信息一致。初始化 SHA-256 验证元数据:
.\gradlew.bat --write-verification-metadata sha256 build如果组织需要验证 PGP 签名,可以使用更严格的元数据类型:
.\gradlew.bat --write-verification-metadata sha256,pgp build常见文件:
gradle/verification-metadata.xml验证元数据通常会记录:
- 构件的 SHA-256 校验和。
- PGP 签名对应的可信 Key。
- 需要忽略或人工确认的元数据来源。
- 校验失败时的组件坐标和文件信息。
第一次生成时,Gradle 会把本地构建下载到的内容写入元数据。因此生成过程本身必须在可信网络、可信仓库和干净环境中完成,不能把任意下载结果直接视为可信输入。
27.11 校验和与签名的取舍
SHA-256 和 PGP 各自解决不同问题:
| 机制 | 主要证明 | 典型风险 |
|---|---|---|
| SHA-256 | 当前下载文件是否与已记录文件相同 | 初始记录若来自被替换文件,仍会被错误信任 |
| PGP | 文件是否由可信密钥签名 | 密钥信任、轮换和密钥来源需要维护 |
| 两者同时使用 | 内容完整性与发布者身份的组合证据 | 配置和维护成本更高 |
更新验证元数据的流程:
- 在可信环境中执行目标依赖升级。
- 只为预期组件写入或更新验证信息。
- 审查
verification-metadata.xml的坐标、哈希和 Key。 - 检查依赖下载来源和发布者签名。
- 在全新 Gradle User Home 中重新构建验证。
- 将元数据与依赖声明、锁文件一起提交。
如果校验失败,先保存完整错误信息和文件哈希,不要直接使用 --offline、删除验证元数据或关闭验证来绕过问题。
27.12 验证元数据的日常命令
查看依赖验证相关帮助:
.\gradlew.bat help --task dependencies.\gradlew.bat help --task dependencyInsight在需要更新验证信息时使用明确的构建范围:
.\gradlew.bat --write-verification-metadata sha256 ` dependencies --configuration runtimeClasspath对于插件和构建逻辑,也要考虑它们的依赖是否被验证。可以在 CI 的全新环境中执行:
.\gradlew.bat --dependency-verification strict clean check实际参数和可用校验级别应以当前 Gradle 版本的 gradle --help 与官方文档为准。项目不应在脚本中静默关闭验证。
27.13 依赖分析与审计命令
掌握依赖图是处理安全问题和版本冲突的前提:
# 查看运行时依赖图.\gradlew.bat dependencies --configuration runtimeClasspath
# 定位某个模块由谁引入.\gradlew.bat dependencyInsight ` --dependency log4j-core ` --configuration runtimeClasspath
# 查看插件和构建逻辑依赖.\gradlew.bat buildEnvironment
# 查看组件对外暴露的变体.\gradlew.bat outgoingVariants审计时应同时关注:
compileClasspath中是否泄漏了不应暴露的实现依赖。runtimeClasspath中是否包含正确的日志、驱动和平台组件。- 测试和质量工具是否引入了与生产不同的高风险依赖。
- 插件、预编译脚本和
buildSrc是否下载或执行额外代码。 - 同一模块是否从多个仓库存在不同来源或元数据。
可以把关键命令输出保存为 CI 构建附件,但要先确认输出不会包含 Token、内部 URL 或其他敏感信息。
27.14 动态版本、变化模块与缓存
以下写法会降低可复现性:
dependencies { implementation("org.example:example-core:latest.release") implementation("org.example:example-api:1.+") implementation("org.example:example-snapshot:1.0-SNAPSHOT")}动态版本、版本范围和变化模块并非绝对禁止,但应限制在明确的开发或实验场景。Release 构建应使用稳定版本、锁文件和验证元数据。
--refresh-dependencies 会要求 Gradle 重新检查依赖来源,不等于忽略锁定或验证:
.\gradlew.bat --refresh-dependencies clean check--offline 只使用本地缓存,适合验证离线可用性,不适合证明远程仓库内容正确:
.\gradlew.bat --offline check不要把手工清理 ~/.gradle/caches 当作常规排错手段。先确认锁文件、验证元数据、仓库配置和缓存行为,再决定是否使用全新 Gradle User Home 重现问题。
27.15 依赖升级与漏洞响应
依赖升级不应只是把旧版本替换成新版本。每次升级至少应记录:
- 原版本、目标版本和最终锁定版本。
- 升级原因:安全修复、Bug 修复、兼容性或功能需求。
- 直接依赖还是传递依赖。
- API、二进制兼容性和运行时行为变化。
- 许可证、签名、校验和发布者变化。
- 测试、性能和部署验证结果。
收到漏洞通报时可以按以下顺序处理:
- 通过
dependencyInsight定位实际引入路径。 - 判断漏洞是否位于编译、运行、测试或构建工具路径。
- 评估是否有可升级版本、约束、替代组件或临时缓解措施。
- 更新版本目录、平台、约束和锁文件。
- 更新验证元数据并审查构件来源。
- 执行受影响模块的回归、集成和安全测试。
- 发布修复版本,并在变更记录中保留漏洞编号和处置结论。
如果漏洞只存在于测试工具或构建插件,也不能简单忽略;构建环境同样可能访问 Secret、源码和发布权限。
27.16 Wrapper、插件与构建逻辑安全
供应链治理必须覆盖 Gradle 本身和构建代码:
gradle-wrapper.jar与gradle-wrapper.properties应一起审查。- Wrapper 版本升级应通过官方发行分发渠道和校验机制完成。
- 插件版本应固定,不要在插件声明中使用动态版本。
buildSrc、Convention Plugin 和初始化脚本应纳入代码审查。- 不要在构建脚本中执行未经审查的远程脚本或命令字符串。
- 只授予 CI 构建完成工作所需的文件系统、网络和 Secret 权限。
- Pull Request 构建不应自动获得生产发布凭据。
构建脚本可以执行任意 JVM 代码,因此“依赖验证只验证应用依赖”是不完整的安全策略。插件和构建逻辑的来源、版本、权限和变更历史同样需要管理。
27.17 SBOM 与依赖清单
SBOM(Software Bill of Materials)描述交付软件包含的组件、版本、许可证和来源。它回答的是“软件由什么组成”,而依赖锁定和验证回答的是“本次构建解析了什么、下载的文件是否可信”。三者不能互相替代。
常见 SBOM 格式包括:
- CycloneDX。
- SPDX。
- 组织内部规定的机器可读格式。
生成 SBOM 时应明确:
- 是为源码项目、构建产物还是容器镜像生成。
- 是否包含运行时、编译时、测试和构建工具依赖。
- 是否保留
group、name、version、PURL、许可证和哈希。 - SBOM 是否与具体 Git Commit、构建号和产物哈希绑定。
- 漏洞扫描结果和例外审批是否与 SBOM 一起归档。
Gradle 本身不等同于某一个 SBOM 生成器。项目可以按组织标准选用 CycloneDX、SPDX 或其他受维护工具,并将其版本固定、配置纳入代码审查。不要把一个静态依赖列表当成完整 SBOM,也不要把生成 SBOM 当成漏洞修复完成。
27.18 CI 依赖治理流水线
一个可执行的 CI 顺序可以是:
checkout -> 校验 Wrapper 与构建入口 -> 解析并验证插件与依赖 -> 检查锁文件和验证元数据无未提交变化 -> 编译、测试、静态分析 -> 生成依赖报告和 SBOM -> 执行漏洞与许可证策略 -> 构建可复现构件 -> 记录哈希、SBOM 和依赖元数据 -> 仅在受保护条件下发布示例命令:
.\gradlew.bat --dependency-verification strict clean check.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat buildCI 还应检查工作区是否被构建任务修改:
git diff --exit-code -- gradle/dependency-locks gradle/verification-metadata.xml如果项目还有 Version Catalog、平台或生成的依赖报告,也应把它们纳入对应的差异检查。升级依赖的 CI 与普通验证 CI 可以分开:普通 CI 拒绝漂移,升级流水线负责提出可审查的变更。
27.19 新依赖准入流程
新增一个第三方依赖时,建议提交以下信息:
- 组件名称、用途和使用位置。
- 直接依赖还是传递依赖。
- 目标版本、许可证和维护状态。
- 官方来源、发布仓库和签名信息。
- 已知漏洞、替代方案和风险接受人。
- 对产物体积、启动时间、运行时权限和数据访问的影响。
- 是否需要
api、implementation、runtimeOnly或compileOnly。 - 锁文件、验证元数据、平台和 SBOM 的预期变化。
可以在 Pull Request 模板中设置检查项:
[ ] 说明依赖用途和引入路径[ ] 确认许可证和维护状态[ ] 检查漏洞和发布来源[ ] 更新 Version Catalog、平台或约束[ ] 更新 Dependency Locking[ ] 更新 Dependency Verification[ ] 运行独立 Consumer 或集成测试[ ] 确认 CI 和 SBOM 结果这种流程的目标不是阻止所有依赖变化,而是让每个变化都具备可追踪的理由和可验证的结果。
27.20 完整依赖治理示例
下面的示例组合仓库集中声明、Version Catalog、锁定、约束和验证相关配置。验证元数据仍然由 Gradle 命令生成并提交,不应在脚本中伪造:
import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement { repositories { gradlePluginPortal() mavenCentral() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() exclusiveContent { forRepository { maven { name = "companyRepository" url = uri("https://repo.example.com/maven") } } filter { includeGroupByRegex("com\\.example(\\..*)?") } } }}configurations.configureEach { resolutionStrategy.activateDependencyLocking()}
dependencies { implementation(libs.commons.lang3) implementation(platform(libs.jackson.bom))
constraints { implementation("org.example:example-core:1.8.2") { because("Approved version for the current release line") } }}先在可信环境生成并审查治理文件:
.\gradlew.bat dependencies --write-locks.\gradlew.bat --write-verification-metadata sha256 clean check再在干净环境执行严格验证:
.\gradlew.bat --dependency-verification strict clean check.\gradlew.bat dependencyInsight --dependency example-core --configuration runtimeClasspath27.21 供应链事件应急处理
发现构件被替换、私钥泄露、恶意版本或高危漏洞时,应先停止发布,再保留证据:
- 记录受影响坐标、版本、仓库、构建号和文件哈希。
- 保留 CI 日志、锁文件、验证元数据和 SBOM 快照。
- 暂停相关 Token、发布权限和自动升级任务。
- 在隔离环境中确认影响范围和首次出现时间。
- 通过新版本、撤回策略或仓库阻断降低消费方风险。
- 重新生成并审查锁文件、验证元数据和 SBOM。
- 用全新环境构建修复版本并执行独立消费验证。
- 发布公告和处置记录,说明受影响范围、修复版本与后续措施。
不要通过删除锁文件、删除校验和、改用不可信镜像或强制接受所有版本来“快速恢复构建”。这些操作可能扩大事件影响并破坏后续取证。
27.22 依赖治理检查清单
解析与版本:
- 关键依赖没有动态版本和未解释的版本区间。
- 共享版本通过 Version Catalog、平台或约束集中管理。
- Release 使用已提交的 Dependency Locking 文件。
- 锁文件更新只包含预期模块和配置变化。
-
api、implementation、runtimeOnly作用域符合设计。
来源与完整性:
- 仓库在
settings.gradle.kts集中声明。 - 内部仓库启用内容过滤或独占内容。
- 关键构件拥有 SHA-256 或 PGP 验证信息。
- 插件、Wrapper 和构建逻辑也纳入审查范围。
- 没有通过删除验证元数据或切换不可信仓库绕过错误。
审计与交付:
- CI 使用严格依赖验证并检查工作区无意外修改。
- 漏洞和许可证扫描有明确的例外审批流程。
- SBOM 与 Git Commit、构建号和产物哈希绑定。
- 依赖升级记录原因、影响、测试和回滚方案。
- Release 仅从受保护分支、Tag 或审批流程发布。
- 发生供应链事件时可以定位影响范围并重新构建。
28. CI/CD 实践
CI/CD 的目标不是把本地命令搬到服务器上,而是建立一条可重复、可审查、可观测、可回滚的交付流水线。Gradle 在其中负责构建模型、任务执行、测试、缓存、依赖解析和构件发布,CI 平台负责触发、隔离、权限、日志、制品保存和环境编排。
一条成熟的 Gradle CI/CD 流程通常包含:
代码变更 -> 依赖与构建入口校验 -> 编译、测试和质量检查 -> 依赖验证与安全审计 -> 构建并保存制品 -> 独立消费验证 -> 预发布或 Staging -> 受保护条件下发布 -> 记录版本、哈希、报告和回滚信息本章使用 GitHub Actions 作为主要示例,同时给出适用于 GitLab CI、Jenkins、TeamCity 等平台的通用设计原则。
28.1 CI 与 CD 的职责边界
CI 主要验证每次变更是否能够安全合并:
- 能否使用 Wrapper 启动正确的 Gradle 版本。
- 依赖、插件和构建逻辑是否能够解析。
- 编译、单元测试、集成测试和静态分析是否通过。
- 锁文件、依赖验证元数据和生成报告是否有意外变化。
- 是否生成预期的 JAR、分发包、SBOM 或其他构件。
CD 主要负责把已经验证的构建结果交付到环境或仓库:
- 将不可变版本发布到 Staging 或制品仓库。
- 通过独立 Consumer 验证已发布坐标。
- 按审批、Tag 或受保护分支推进 Release。
- 保存构件哈希、签名、SBOM、日志和部署记录。
- 在失败时停止后续阶段,并提供明确的重试或回滚路径。
不要在 CD 阶段重新编译一份“看起来相同”的代码再发布。更可靠的做法是保存 CI 已验证的构件,在后续阶段提升同一份构件。
28.2 CI 中的推荐命令
最小验证命令:
./gradlew clean check --no-daemon --stacktrace构建和发布前检查:
./gradlew clean build --no-daemon --stacktrace./gradlew dependencies --configuration runtimeClasspath./gradlew dependencyInsight --dependency example-core --configuration runtimeClasspath依赖供应链验证:
./gradlew --dependency-verification strict clean check --no-daemon发布流水线可以在独立阶段执行:
./gradlew publishAllPublicationsToStagingDirectoryRepository --no-daemon./gradlew publishMavenJavaPublicationToInternalRepository --no-daemon命令选择原则:
- 普通 Pull Request 优先执行
check,不要默认执行远程发布。 build是否包含完整打包,取决于项目任务图,应通过tasks和build依赖关系确认。- 不要把
--rerun-tasks作为常规 CI 参数,它会绕过增量和缓存,适合专门的重跑验证。 - 不要默认使用
--refresh-dependencies,依赖更新应由独立升级流程触发。 - 出现难以定位的失败时临时加入
--info或--scan,避免所有构建永久使用高噪声日志。
28.3 Wrapper、JDK 与 Toolchain
CI 必须使用仓库中的 Wrapper:
./gradlew --version./gradlew help --no-daemonWindows Runner 使用:
.\gradlew.bat --version.\gradlew.bat help --no-daemon固定 JDK 版本有两层作用:
- CI Action 或 Runner 使用明确的 JDK 运行 Gradle。
- Gradle Java Toolchain 为编译、测试和 Javadoc 选择明确的 Java 工具链。
示例:
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}CI 仍应记录以下信息:
./gradlew --versionjava -version升级 Wrapper、JDK 或 Toolchain 时,应把它视为构建基础设施变更,执行完整测试、缓存验证、发布验证和兼容性检查,而不是只修改一个版本字符串。
28.4 GitHub Actions 基础工作流
一个最小的 GitHub Actions 工作流如下:
name: Gradle build
on: push: branches: [main] pull_request:
permissions: contents: read
jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6
- name: Setup Java uses: actions/setup-java@v5 with: distribution: temurin java-version: '21'
- name: Setup Gradle uses: gradle/actions/setup-gradle@v6
- name: Verify run: ./gradlew check --no-daemon --stacktrace这个工作流具备几个重要特征:
- 使用
checkout获取当前提交,而不是默认构建 Runner 上的旧目录。 - 使用明确的 JDK 发行版和版本。
- 通过
setup-gradle统一配置 Gradle User Home 和缓存能力。 - 使用 Wrapper,而不是依赖 Runner 全局安装的 Gradle。
- 使用
permissions收紧默认 Token 权限。
Action 版本会随平台和组织策略变化。项目应定期检查当前官方 Action 文档,并通过 Dependabot、Renovate 或人工审查维护版本,而不是把示例版本视为永久不变。
28.5 事件、权限与并发控制
CI 工作流应区分 Pull Request、主分支、Tag 和定时任务:
on: pull_request: push: branches: - main tags: - 'v*' schedule: - cron: '17 2 * * 1'对于同一个分支或 Pull Request,可以取消过时的运行:
concurrency: group: gradle-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true权限应遵循最小授权:
permissions: contents: read只有确实需要时才增加权限,例如上传依赖图、创建 Release 或写入部署状态。来自 Fork 的 Pull Request 不应获得发布 Token、签名私钥或生产环境凭据。
28.6 Pull Request、主分支与 Release 分层
推荐把流水线分成三个层级:
| 层级 | 触发时机 | 主要任务 | 是否允许发布 |
|---|---|---|---|
| 快速反馈 | Pull Request | 编译、单元测试、静态分析、依赖验证 | 否 |
| 集成验证 | main Push | 完整测试、构建、报告、SBOM、缓存写入 | 通常否 |
| Release | 受保护 Tag 或审批 | Staging、签名、独立消费验证、正式发布 | 是 |
Pull Request 任务应尽量快速,但不能为了速度关闭关键安全检查。可以把耗时较长的端到端测试和多平台矩阵放在主分支或合并队列,同时保留一组有代表性的快速测试作为合并门禁。
28.7 Java 与操作系统矩阵
当项目需要兼容多个 JDK 或操作系统时,使用矩阵而不是复制多份工作流:
jobs: test: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] java: ['17', '21'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6
- uses: actions/setup-java@v5 with: distribution: temurin java-version: ${{ matrix.java }}
- uses: gradle/actions/setup-gradle@v6
- name: Test run: ./gradlew check --no-daemon --stacktrace矩阵设计需要考虑成本和覆盖率:
- 主要开发平台可以执行完整测试矩阵。
- 非主平台可以只执行关键测试或定时执行。
fail-fast: false便于一次看到所有平台失败,而不是首个失败后取消其他任务。- 矩阵任务应上传测试报告,否则失败原因可能只存在于短暂 Runner 日志中。
- 兼容性矩阵不等于所有 JDK 都必须用于发布,Release 应指定唯一受支持的构建 JDK。
28.8 Gradle User Home 缓存
CI 缓存通常包含以下内容:
- Wrapper 分发包。
- 依赖模块和元数据缓存。
- 编译后的构建脚本和插件缓存。
- Artifact Transform 输出。
- 本地 Build Cache。
使用 Gradle 官方 GitHub Action 时,通常不应再叠加另一套缓存 Gradle User Home 的 Action:
- name: Setup Gradle uses: gradle/actions/setup-gradle@v6如果选择手工缓存,应明确缓存范围和失效键:
- name: Cache Gradle User Home uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/*.versions.toml') }}缓存键至少应考虑:
- 操作系统和 CPU 架构。
- Wrapper 版本。
- JDK 版本或 Toolchain 配置。
settings.gradle.kts、构建脚本和 Convention Plugin。- Version Catalog、平台、锁文件和验证元数据。
缓存只能加速构建,不能成为构建成功的前提。清空缓存后仍应能够从可信仓库完成构建。
28.9 Build Cache 与 Configuration Cache
Build Cache 和 Configuration Cache 解决不同阶段的问题:
| 缓存 | 缓存内容 | 主要收益 |
|---|---|---|
| Incremental Build | 当前工作区任务输入输出关系 | 避免重复执行未变化任务 |
| Build Cache | 可复用的任务输出和 Artifact Transform 输出 | 跨工作区、开发者和 CI Agent 复用结果 |
| Configuration Cache | 配置阶段生成的任务图和配置结果 | 减少配置时间,部分场景下改善任务执行 |
本地尝试 Build Cache:
./gradlew clean check --build-cache项目级启用:
org.gradle.caching=trueConfiguration Cache 应逐步启用并处理兼容性问题:
./gradlew check --configuration-cache在临时 CI Runner 中,配置缓存只有命中并且缓存能够跨 Job 保留时才可能带来收益。可以按项目和 CI 平台能力选择读写策略,不要为了“开启缓存”而忽略插件兼容性、Secret 泄露和缓存隔离问题。
28.10 远程 Build Cache
远程 Build Cache 可以让多个开发者和 CI Agent 复用任务输出。基本配置示意:
buildCache { local { isEnabled = true }
remote<HttpBuildCache> { url = uri("https://cache.example.com/cache/") isPush = providers.environmentVariable("CI").isPresent credentials { username = providers.environmentVariable("BUILD_CACHE_USER").orNull password = providers.environmentVariable("BUILD_CACHE_PASSWORD").orNull } }}远程缓存治理要特别注意:
- Pull Request 和不可信分支通常只读,不能向共享缓存写入结果。
- 只有输入、输出和环境声明完整的任务才适合缓存。
- 构建输出中不得包含 Token、私钥、工作区敏感数据或环境专属文件。
- 缓存服务需要认证、传输加密、访问审计和容量管理。
- 不同 JDK、操作系统、架构或工具链不应错误共享不兼容输出。
- 远程缓存故障不应让构建完全无法完成;应允许回退到本地或无缓存执行。
远程缓存不是制品仓库,也不是发布渠道。Release 构件必须从明确的构建任务生成并独立保存。
28.11 Configuration Cache 在 CI 中的注意事项
Configuration Cache 会记录配置阶段读取的文件、系统属性、环境变量和任务图。CI 中启用前需要检查:
- 构建逻辑是否在执行阶段访问了不可序列化的 Project 对象。
- 任务是否正确声明输入、输出和环境依赖。
- CI 分支、Secret、构建参数是否被安全地作为配置输入处理。
- Runner 是否复用相同的 Gradle User Home。
- 缓存条目是否会被不可信作业读取。
排查配置缓存问题:
./gradlew check --configuration-cache --configuration-cache-problems=warnwarn 适合迁移和定位问题,不应长期作为忽略不兼容的替代方案。完成迁移后应恢复严格失败行为,并关注每次 Gradle 或插件升级带来的兼容性变化。
如果 CI 使用短生命周期 Runner,可以考虑只读取已有配置缓存:
./gradlew check \ -Dorg.gradle.configuration-cache=true \ -Dorg.gradle.configuration-cache.read-only=true该策略是否有效取决于缓存是否跨 Job 保留;没有命中时,读写缓存都可能增加复杂度而没有实际收益。
28.12 测试报告与构建制品
失败时最有价值的不是一行 Process completed with exit code 1,而是测试报告、日志、构建扫描链接和实际输出。可以在工作流中上传报告:
- name: Upload reports if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: gradle-reports-${{ matrix.os }}-java-${{ matrix.java }} path: | **/build/reports/** **/build/test-results/** **/build/logs/** if-no-files-found: ignore发布或交付构件:
- name: Build distributions run: ./gradlew build distZip --no-daemon
- name: Upload distributions uses: actions/upload-artifact@v4 with: name: distributions path: | **/build/libs/*.jar **/build/distributions/*制品上传前应排除:
- 包含密码、Token 或私钥的文件。
- 临时目录和测试缓存。
- 不属于发布范围的中间构件。
- 不同矩阵任务生成但未标记平台和 JDK 的同名文件。
制品名称应包含模块、版本、操作系统、JDK 或测试分片信息,避免多个 Job 上传同名文件后难以追踪。
28.13 测试分层与并行 Job
大型项目可以把单一 check 拆成多个 Job:
validate-build -> unit-test -> integration-test -> static-analysis -> package -> publish-staging拆分时要注意:
- Job 之间的依赖关系应通过
needs明确表达。 - 每个 Job 都应能在干净 Runner 中初始化 Gradle。
- 不要因为并行而重复消耗大量依赖下载和缓存写入。
- 只有所有必需验证通过,打包或发布 Job 才能继续。
- 集成测试需要数据库、消息队列等服务时,应明确服务版本和生命周期。
示例:
jobs: unit-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' - uses: gradle/actions/setup-gradle@v6 - run: ./gradlew test --no-daemon
package: needs: unit-test runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' - uses: gradle/actions/setup-gradle@v6 - run: ./gradlew jar sourcesJar --no-daemon如果后续 Job 需要前一 Job 的构件,应通过制品上传和下载传递,而不是依赖共享工作区或碰巧存在的 Runner 缓存。
28.14 依赖、验证与 CI 安全门禁
第 27 章中的依赖锁定和验证应在 CI 中变成可执行门禁:
./gradlew --dependency-verification strict clean check --no-daemon可以额外检查构建是否修改治理文件:
git diff --exit-code -- \ gradle/dependency-locks \ gradle/verification-metadata.xml \ gradle/libs.versions.toml依赖更新流水线与普通验证流水线应分离:
- 普通 CI 拒绝未提交的锁文件或验证元数据变化。
- 升级任务在独立分支生成变更并提交 Pull Request。
- 安全升级可以走加急审批,但仍保留测试和来源审查。
- SBOM、漏洞扫描和许可证扫描结果应保存为构建记录。
- 构建脚本和插件依赖也要纳入审计,不应只扫描运行时 JAR。
28.15 Secret、OIDC 与发布权限
普通构建不应拥有发布凭据。发布 Job 应具备更严格的边界:
jobs: publish: if: startsWith(github.ref, 'refs/tags/v') permissions: contents: read id-token: write environment: name: production runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' - uses: gradle/actions/setup-gradle@v6 - name: Publish env: MAVEN_REPO_USERNAME: ${{ secrets.MAVEN_REPO_USERNAME }} MAVEN_REPO_PASSWORD: ${{ secrets.MAVEN_REPO_PASSWORD }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} run: ./gradlew publish --no-daemon安全原则:
- Secret 只注入需要它的 Job 和 Step。
- 不在命令行参数、日志、构建扫描或错误信息中打印 Secret。
- Fork Pull Request 不执行发布 Job。
- 使用 Environment Protection Rules、审批和受保护 Tag 控制 Release。
- 支持时优先使用短期身份令牌或 OIDC,而不是长期静态密钥。
- 发布账号、签名密钥和远程缓存账号分开管理。
即使使用 OIDC,也需要仓库端配置受信任的工作流、分支、Tag 和环境条件。
28.16 构件提升与发布流水线
推荐的 CD 流程是“构建一次,多处提升”:
main 验证 -> 生成版本化构件 -> 上传 CI 制品或内部制品库 -> Staging Consumer 验证 -> 人工或策略审批 -> 将同一构件提升到 Release 仓库如果仓库不支持构件提升,至少应保存以下信息并在正式发布前重新确认:
- Git Commit 和 Tag。
- Gradle Wrapper、JDK 和操作系统。
- 构件 SHA-256。
- POM、
.module、签名和 SBOM。 - CI Workflow Run、构建参数和发布仓库。
- 独立 Consumer 的测试结果。
不要用 SNAPSHOT 构件作为正式 Release 的输入,也不要在同一坐标下覆盖已发布的不可变版本。
28.17 发布前 Staging 验证
发布前可以先生成本地或远程 Staging 仓库:
./gradlew clean check./gradlew publishAllPublicationsToStagingDirectoryRepository然后用独立项目验证:
repositories { maven { url = uri("../producer/build/staging-repo") }}
dependencies { implementation("com.example:example-core:1.0.0-rc1")}验证项包括:
- Gradle Consumer 能解析正确变体和传递依赖。
- Maven Consumer 能读取正确 POM。
- 主 JAR、sources、javadoc 和签名文件齐全。
- API、运行时行为和 Java 版本兼容。
- SBOM 中的版本和构件哈希与发布物一致。
- 发布仓库没有混入测试、Snapshot 或本地路径。
28.18 CI 失败诊断
失败诊断应先区分失败层级:
| 现象 | 优先检查 |
|---|---|
| Wrapper 启动失败 | Wrapper 文件、网络、JDK 和分发包校验 |
| 依赖解析失败 | 仓库地址、凭据、锁文件、验证元数据和缓存 |
| 配置阶段失败 | Plugin、Convention Plugin、Configuration Cache 报告 |
| 编译或测试失败 | JDK、Toolchain、测试报告、平台差异 |
| 缓存命中后失败 | 任务输入输出声明、环境差异、缓存隔离 |
| 发布失败 | 坐标、凭据、签名、仓库策略和不可变版本规则 |
临时提高日志:
./gradlew check --info --stacktrace./gradlew check --scan对缓存问题可以做对照实验:
./gradlew clean check --no-build-cache --no-configuration-cache如果禁用缓存后仍然失败,问题通常不是缓存本身;如果只在缓存命中时失败,应检查任务是否把隐式环境输入正确声明为任务输入。
失败 Job 应上传:
build/reports。build/test-results。- Configuration Cache 报告。
- 必要的 Gradle 日志和 Build Scan 链接。
- 版本、JDK、操作系统和命令行参数。
28.19 定时任务与升级验证
除了 Push 和 Pull Request,建议设置定时流水线:
on: schedule: - cron: '17 2 * * 1'定时任务可以执行:
- 最新稳定 JDK 或 Gradle Release Candidate 的兼容性试跑。
- 依赖漏洞和许可证扫描。
- 锁文件、验证元数据和仓库可用性检查。
- 多平台集成测试。
- 缓存命中率和构建耗时趋势采集。
- SBOM 生成和依赖图提交。
定时任务不应直接修改主分支。它应生成报告、Issue 或 Pull Request,让升级结果经过与普通代码变更相同的审查流程。
28.20 其他 CI 平台的通用实现
无论使用哪个 CI 平台,都可以映射为以下几个阶段:
prepare - checkout - install or validate JDK - validate Gradle Wrapper
verify - dependency verification - compile - test - quality checks
package - jar - distribution - SBOM - checksums
publish - staging - consumer verification - release promotionGitLab CI 示例:
gradle-check: image: eclipse-temurin:21-jdk script: - ./gradlew --version - ./gradlew --dependency-verification strict clean check --no-daemon artifacts: when: always paths: - '**/build/reports/' - '**/build/test-results/'Jenkins、TeamCity 等平台同样应使用 Wrapper、固定 JDK、独立工作区、受控 Secret、可追踪制品和明确的缓存策略。平台语法不同,但构建入口和质量门禁应尽量保持一致。
28.21 完整 GitHub Actions 示例
下面的示例包含快速验证、矩阵测试、报告上传、构件打包和受保护发布的基本结构:
name: Gradle CI/CD
on: pull_request: push: branches: [main] tags: ['v*'] schedule: - cron: '17 2 * * 1'
permissions: contents: read
concurrency: group: gradle-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true
jobs: verify: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] java: ['17', '21'] runs-on: ${{ matrix.os }} steps: - name: Checkout uses: actions/checkout@v6
- name: Setup Java uses: actions/setup-java@v5 with: distribution: temurin java-version: ${{ matrix.java }}
- name: Setup Gradle uses: gradle/actions/setup-gradle@v6
- name: Verify build shell: bash run: ./gradlew --dependency-verification strict clean check --no-daemon --stacktrace
- name: Upload reports if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: reports-${{ matrix.os }}-java-${{ matrix.java }} path: | **/build/reports/** **/build/test-results/** if-no-files-found: ignore
package: if: github.event_name != 'pull_request' needs: verify runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6
- name: Setup Java uses: actions/setup-java@v5 with: distribution: temurin java-version: '21'
- name: Setup Gradle uses: gradle/actions/setup-gradle@v6
- name: Build artifacts run: ./gradlew clean build distZip --no-daemon
- name: Upload artifacts uses: actions/upload-artifact@v4 with: name: gradle-artifacts path: | **/build/libs/*.jar **/build/distributions/*
publish: if: startsWith(github.ref, 'refs/tags/v') needs: package environment: name: production permissions: contents: read id-token: write runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6
- name: Setup Java uses: actions/setup-java@v5 with: distribution: temurin java-version: '21'
- name: Setup Gradle uses: gradle/actions/setup-gradle@v6
- name: Publish release env: MAVEN_REPO_USERNAME: ${{ secrets.MAVEN_REPO_USERNAME }} MAVEN_REPO_PASSWORD: ${{ secrets.MAVEN_REPO_PASSWORD }} SIGNING_KEY: ${{ secrets.SIGNING_KEY }} SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} run: ./gradlew publish --no-daemon --stacktrace生产项目还应补充构件提升、Staging Consumer、SBOM 上传、签名验证和回滚策略。示例展示的是结构,不代表所有项目都应直接复制。
28.22 CI 性能优化顺序
不要一开始就堆叠所有缓存和并行选项。推荐按以下顺序优化:
- 确认任务输入输出完整,消除无意义的重复执行。
- 使用 Wrapper、固定 JDK 和稳定依赖缓存。
- 启用本地 Build Cache,检查
FROM-CACHE和命中率。 - 评估远程 Build Cache 的成本、隔离和安全边界。
- 修复插件和构建逻辑后逐步启用 Configuration Cache。
- 将独立测试和质量任务拆分为并行 Job。
- 通过 Build Scan、CI 日志和趋势数据定位真正的瓶颈。
优化前后应记录:
- 总耗时和 P95/P99 耗时。
- 配置时间、执行时间和下载时间。
- 任务成功、跳过和缓存命中数量。
- Runner 成本、缓存存储和网络流量。
- 失败率、重试率和排队时间。
“缓存越多越快”并不成立。失效频繁、恢复缓慢、命中错误或维护成本过高的缓存,可能让整体流水线更慢。
28.23 CI/CD 检查清单
构建一致性:
- 所有 CI 构建使用 Wrapper。
- JDK、Toolchain、操作系统和 Gradle 版本明确。
- Pull Request、主分支、Tag 和定时任务职责分离。
- 依赖锁定、验证元数据和 SBOM 纳入 CI 检查。
- 清空缓存后仍可以从可信来源完成构建。
缓存与性能:
- Gradle User Home 缓存不会被重复机制互相覆盖。
- Build Cache 只缓存输入输出声明完整的任务。
- 不可信分支不能写入共享远程缓存。
- Configuration Cache 问题已处理,而不是长期忽略。
- 性能优化通过指标验证,并考虑存储和 Runner 成本。
安全与发布:
- 普通验证 Job 不拥有发布和签名 Secret。
- 发布只能由受保护 Tag、分支或环境审批触发。
- 发布使用已验证构件,避免重新编译产生漂移。
- 构件、报告、SBOM、签名和哈希可以追溯到 Commit。
- Staging Consumer 验证通过后才能正式发布。
- 失败、重试、暂停和回滚路径有明确记录。
29. Android Gradle 基础
Android 项目由 Gradle、Android Gradle Plugin(AGP)、Android SDK、JDK、Kotlin 或 Java 编译工具共同完成构建。Gradle 提供构建模型和任务执行能力,AGP 则扩展出 Android 专用的 DSL、变体、资源处理、Manifest 合并、APK/AAB 打包、签名和设备测试任务。
Android 构建排错不能只看一个版本号。至少要同时核对:
- AGP 版本与 Gradle 版本。
- Android Studio 版本与 JDK 版本。
compileSdk、targetSdk、minSdk和 Build Tools。- Kotlin、KSP、Compose 或其他编译插件版本。
- AndroidX、测试框架和第三方库的兼容关系。
- 本地 SDK、模拟器、物理设备和 CI 镜像是否一致。
本章使用 Kotlin DSL 说明核心概念。示例中的版本号只是结构示例,真实项目必须以 Android Developers 的兼容表、项目实际约束和依赖发布信息为准。
29.1 Android 项目结构
一个典型的单 App 项目如下:
android-project/├── settings.gradle.kts├── build.gradle.kts├── gradle.properties├── gradle/│ ├── libs.versions.toml│ └── wrapper/├── app/│ ├── build.gradle.kts│ └── src/│ ├── main/│ │ ├── AndroidManifest.xml│ │ ├── java/ 或 kotlin/│ │ └── res/│ ├── debug/│ ├── release/│ ├── test/│ └── androidTest/└── gradlew.bat各文件职责不同:
| 文件或目录 | 主要职责 |
|---|---|
settings.gradle.kts | 定义构建名称、模块、插件仓库和依赖仓库边界 |
根 build.gradle.kts | 声明插件版本、共享构建逻辑或应用 Convention Plugin |
模块 build.gradle.kts | 配置 Android App、Library、变体、依赖和打包 |
gradle.properties | 配置 Gradle 或项目属性,不应存放提交到仓库的 Secret |
src/main | 所有变体共享的源码、资源和 Manifest |
src/debug、src/release | Build Type 专属输入 |
src/<flavor> | Product Flavor 专属输入 |
src/test | 本地 JVM 单元测试 |
src/androidTest | 设备或模拟器上的 Instrumentation 测试 |
29.2 Settings、插件与仓库
settings.gradle.kts 应集中声明插件和依赖仓库:
import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() }}
rootProject.name = "android-project"include(":app")include(":core", ":feature:home")根 build.gradle.kts 通常只声明插件版本:
plugins { id("com.android.application") version "9.3.0" apply false id("com.android.library") version "9.3.0" apply false id("org.jetbrains.kotlin.android") version "2.3.20" apply false}apply false 表示插件可供子项目使用,但不应用于根项目。不要在多个模块重复声明同一个插件版本,也不要在项目脚本中随意增加未审查的仓库。
29.3 AGP、Gradle、JDK 与 SDK 兼容性
Android 构建环境至少包含四层兼容关系:
Android Studio / CI 镜像 ↓AGP 版本 ↔ Gradle Wrapper 版本 ↔ JDK 版本 ↓compileSdk / Build Tools / SDK Platform排查版本问题时建议按以下顺序:
- 从
gradle/wrapper/gradle-wrapper.properties读取 Wrapper 版本。 - 从根构建脚本或 Version Catalog 读取 AGP 版本。
- 使用
java -version和./gradlew --version确认实际 JDK。 - 检查
compileSdk、Build Tools 和本地 SDK 是否已安装。 - 对照 AGP 官方兼容表,而不是根据经验猜测组合。
- 在本地和 CI 使用相同的 JDK、SDK 镜像和 Wrapper。
建议显式声明 Java Toolchain 或在 CI 镜像中固定 JDK。不要只依赖 Android Studio 当前选择的 JDK,因为命令行和 CI 可能使用另一套环境。
29.4 App 模块与 Library 模块
App 模块生成可以安装或分发的 APK/AAB,Library 模块生成供其他模块消费的 Android Archive(AAR)。两者的插件和配置目标不同:
plugins { id("com.android.application") id("org.jetbrains.kotlin.android")}
android { namespace = "com.example.app" compileSdk = 36
defaultConfig { applicationId = "com.example.app" minSdk = 26 targetSdk = 36 versionCode = 1 versionName = "1.0" }}plugins { id("com.android.library") id("org.jetbrains.kotlin.android")}
android { namespace = "com.example.core" compileSdk = 36
defaultConfig { minSdk = 26 }}常见原则:
applicationId属于 App 的安装和发布身份,Library 通常不设置它。namespace用于生成R、BuildConfig和源码包名边界,必须稳定且唯一。- Library 的
minSdk不应高于它所支持的消费方,除非项目明确接受该限制。 - App 可以依赖多个 Library,Library 之间也可以形成清晰的模块依赖图。
- 业务功能、数据层和通用组件优先拆成 Library,避免所有代码堆在
app模块。
29.5 Android DSL 与配置边界
模块级 android {} DSL 由 AGP 提供。它描述的是 Android 构建模型,不应把复杂业务逻辑全部塞进其中:
android { namespace = "com.example.app" compileSdk = 36
defaultConfig { minSdk = 26 targetSdk = 36 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" }
buildFeatures { buildConfig = true viewBinding = true }}配置建议:
- 适合放在
android {}中的内容:SDK、变体、签名、资源、Manifest、Lint、构建特性。 - 适合放在
dependencies {}中的内容:模块依赖、平台、测试依赖和运行时依赖。 - 适合放在 Convention Plugin 中的内容:所有模块共享的 Java/Kotlin、测试、Lint 和发布规则。
- 适合放在普通 Gradle Task 中的内容:可声明输入输出的代码生成、校验和打包辅助工作。
- 不要在配置阶段访问设备、网络或执行耗时外部命令。
29.6 Android 常用任务
查看 Android 相关任务:
.\gradlew.bat tasks --all常用构建、测试和检查任务:
.\gradlew.bat assembleDebug.\gradlew.bat assembleRelease.\gradlew.bat bundleRelease.\gradlew.bat installDebug.\gradlew.bat testDebugUnitTest.\gradlew.bat connectedDebugAndroidTest.\gradlew.bat lintDebug.\gradlew.bat :app:signingReport任务含义:
assembleDebug:生成 Debug APK,不负责安装。assembleRelease:生成 Release APK,是否可分发取决于签名和发布配置。bundleRelease:生成 Android App Bundle,通常用于应用商店分发。installDebug:构建并安装 Debug 变体到设备或模拟器。testDebugUnitTest:运行 Debug 对应的本地 JVM 测试。connectedDebugAndroidTest:在已连接设备或模拟器上执行 Instrumentation 测试。lintDebug:对 Debug 变体执行 Android Lint。signingReport:查看模块和变体的签名信息。
29.7 Build Type
Build Type 表示开发、测试和发布阶段的构建属性。AGP 通常提供 debug 和 release:
android { buildTypes { debug { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" isDebuggable = true }
release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) }
create("staging") { initWith(getByName("debug")) applicationIdSuffix = ".staging" versionNameSuffix = "-staging" matchingFallbacks += listOf("debug") } }}Build Type 常用属性:
isDebuggable:是否允许调试。applicationIdSuffix:为不同阶段生成不同安装 ID。versionNameSuffix:在版本名中体现构建阶段。isMinifyEnabled:是否执行代码压缩和优化。isShrinkResources:是否移除未使用资源。proguardFiles:配置 R8/ProGuard 规则。signingConfig:指定签名配置。
不要把 Release 直接复制成 Debug。Debug 应便于开发和诊断,Release 应强调可分发、最小体积、稳定签名和可追踪构建。
29.8 Product Flavor 与维度
Product Flavor 用于表达免费版、付费版、国内版、海外版或不同后端环境等产品差异:
android { flavorDimensions += "tier"
productFlavors { create("free") { dimension = "tier" applicationIdSuffix = ".free" versionNameSuffix = "-free" buildConfigField("String", "API_ENV", "\"free\"") } create("paid") { dimension = "tier" applicationIdSuffix = ".paid" versionNameSuffix = "-paid" buildConfigField("String", "API_ENV", "\"paid\"") } }}所有 Flavor 都必须归属于明确的 flavor dimension。多个维度会产生笛卡尔积:
free × china × debugfree × china × releasepaid × china × debugpaid × china × releasefree × global × debugfree × global × releasepaid × global × debugpaid × global × release维度数量和每个维度的 Flavor 数量会直接增加变体数量。变体过多会扩大编译、测试、Lint、资源处理和 CI 矩阵,应先确认产品确实需要每一种组合。
29.9 Build Variant 与任务命名
Build Variant 是 Build Type 与 Product Flavor 的组合。假设只有 free、paid 两个 Flavor 和 debug、release 两个 Build Type,AGP 会生成:
freeDebugfreeReleasepaidDebugpaidRelease对应任务通常使用变体名称拼接:
.\gradlew.bat assembleFreeDebug.\gradlew.bat assemblePaidRelease.\gradlew.bat testFreeDebugUnitTest.\gradlew.bat lintPaidRelease.\gradlew.bat bundlePaidRelease通过任务名定位变体时注意大小写:Gradle 任务名使用 Camel Case,而目录通常使用小写或驼峰组合。
查看可用变体:
.\gradlew.bat :app:tasks --all | Select-String -Pattern 'assemble|bundle|test|lint'不要假设所有模块拥有完全相同的变体。如果 App 有 free Flavor,而 Library 没有对应 Flavor,可能需要 Variant Matching 或 matchingFallbacks。
29.10 Android Source Set 与覆盖优先级
典型 Source Set:
src/main/src/debug/src/release/src/free/src/freeDebug/src/test/src/androidTest/源码和资源合并时,变体专属 Source Set 的优先级通常高于 main。对于 freeDebug,可以把它理解为多个输入集合共同参与:
main + free + debug + freeDebug适合放入不同 Source Set 的内容:
main:所有版本共享的业务逻辑和资源。debug:调试菜单、日志、Mock 服务和诊断页面。release:发布专属配置或受限实现。free、paid:产品功能、品牌和资源差异。freeDebug:只属于某一组合的特殊输入。test:本地单元测试。androidTest:设备测试和测试资源。
当多个 Source Set 提供同名资源或类时,不要只凭目录猜测结果。使用 sourceSets、构建报告和实际产物验证合并后的内容。
29.11 变体感知依赖
Android Library 可能为不同变体发布不同依赖和属性。App 的 debug 变体通常会优先匹配 Library 的 debug 变体,Flavor 也会参与匹配:
dependencies { implementation(project(":core")) freeImplementation("com.example:free-sdk:1.0.0") debugImplementation("com.example:debug-tools:1.0.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")}如果 App 使用 staging Build Type,而某个 Library 只有 debug 和 release,可以配置回退:
android { buildTypes { create("staging") { matchingFallbacks += listOf("debug") } }}常见匹配失败原因:
- App 和 Library 的 flavor dimension 名称不一致。
- App 请求的 Flavor 在 Library 中不存在。
- 自定义 Build Type 没有
matchingFallbacks。 - 依赖模块的属性、能力或 Android SDK 要求不兼容。
- 依赖被错误声明为某个不存在的变体配置。
29.12 SDK、Java 与 Kotlin 编译选项
compileSdk 决定编译时可见的 Android API,minSdk 决定应用支持的最低系统版本,targetSdk 表达应用针对的行为兼容目标。三者不是同一个概念:
android { compileSdk = 36
defaultConfig { minSdk = 26 targetSdk = 36 }}建议同时统一 Java 和 Kotlin 的目标版本:
android { compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 }}
kotlin { jvmToolchain(17)}升级 compileSdk 不等于自动解决运行时兼容性。仍然需要:
- 根据
minSdk处理新 API 的版本判断。 - 使用 Android Lint 检查 API 调用风险。
- 在目标设备和低版本设备上执行测试。
- 检查第三方库的最低 SDK 要求。
- 让 CI 镜像预装准确的 SDK Platform 和 Build Tools。
29.13 依赖与 Version Catalog
Android 项目可以使用 Version Catalog 统一管理插件和库:
[versions]agp = "9.3.0"kotlin = "2.3.20"androidxCore = "1.17.0"
[plugins]android-application = { id = "com.android.application", version.ref = "agp" }kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
[libraries]androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }pluginManagement { includeBuild("build-logic")}plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android)}
dependencies { implementation(libs.androidx.core)}目录可以统一坐标和版本,但不能代替依赖锁定、验证和漏洞扫描。AGP、Kotlin、Compose、KSP 或其他插件的版本必须结合兼容矩阵一起升级。
29.14 Manifest 合并与资源合并
最终 APK/AAB 中的 Manifest 不是只来自 src/main/AndroidManifest.xml。它可能由 App、Library、Flavor、Build Type 和生成配置共同参与合并。
遇到 Manifest 冲突时,先查看合并报告,再决定是否使用 tools:replace、tools:node 或 tools:remove:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<application android:label="@string/app_name" tools:replace="android:label" /></manifest>不要为了消除错误而大范围使用 tools:replace。每个覆盖规则都应说明:
- 被覆盖的来源是哪一个 Library 或 Source Set。
- 为什么 App 的值更正确。
- 是否会影响权限、导出组件、Provider 或安全属性。
- 是否需要在不同变体中使用不同的规则。
资源冲突也应按照来源、优先级和最终产物逐项分析,而不是随意重命名所有资源。
29.15 BuildConfig、Manifest Placeholder 与环境配置
构建变体可以生成类型化的 BuildConfig 字段和 Manifest 占位符:
android { defaultConfig { buildConfigField("String", "API_BASE_URL", "\"https://api.example.com\"") manifestPlaceholders["providerAuthority"] = "com.example.app.files" }
buildTypes { debug { buildConfigField("String", "API_BASE_URL", "\"https://dev-api.example.com\"") } }}Manifest 中使用占位符:
<provider android:name="androidx.core.content.FileProvider" android:authorities="${providerAuthority}" android:exported="false" android:grantUriPermissions="true" />安全边界:
- API 地址、功能开关和版本信息可以通过构建配置注入。
- API 密钥、数据库密码、签名口令和 Token 不应写入 APK/AAB 的任何配置。
- 只要进入应用包,用户就有可能提取;“放在
BuildConfig中”不是保密方案。 - 生产环境配置应通过安全服务、服务器端控制或受保护的发布流程提供。
29.16 签名配置与 APK/AAB 交付
Debug 构建通常使用自动生成的 Debug Key,Release 构建需要组织管理的发布签名:
android { signingConfigs { create("release") { storeFile = file(providers.gradleProperty("releaseStoreFile").get()) storePassword = providers.gradleProperty("releaseStorePassword").get() keyAlias = providers.gradleProperty("releaseKeyAlias").get() keyPassword = providers.gradleProperty("releaseKeyPassword").get() } }
buildTypes { release { signingConfig = signingConfigs.getByName("release") } }}上面的属性只能来自用户级 ~/.gradle/gradle.properties 或 CI Secret,不能提交到 Git。更安全的生产实践还应考虑:
- 使用 CI 的受保护 Secret 和最小权限服务账号。
- 对签名文件、密钥库和口令进行轮换与备份。
- 通过
signingReport和发布流水线确认实际使用的签名。 - Release 构建完成后验证 APK/AAB 签名和产物哈希。
- 不在日志、构建扫描或异常输出中打印口令。
常见交付任务:
.\gradlew.bat assembleRelease.\gradlew.bat bundleRelease.\gradlew.bat :app:signingReportAPK 适合直接安装或测试,AAB 适合交给应用商店根据设备配置生成分发 APK。二者的发布、签名和验证流程应明确区分。
29.17 R8、代码压缩与资源收缩
Release 常用 R8 完成代码压缩、优化、混淆和无用代码移除:
android { buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } }}启用 R8 后要关注:
- 反射、序列化、JNI、动态加载和依赖注入框架的保留规则。
- 第三方 Library 提供的 consumer ProGuard/R8 规则。
mapping.txt、seeds.txt、usage.txt等诊断文件的归档。- 崩溃平台的符号化和版本对应关系。
- Release、Staging 和 Debug 是否需要不同的压缩策略。
不要通过添加大范围 -keep class ** { *; } 解决所有问题。应定位被移除的类、反射入口或资源,再添加最小规则并补充回归测试。
29.18 Android Lint 与测试
Lint 是 Android 构建中的静态分析环节,不能替代单元测试、设备测试或安全评审:
android { lint { abortOnError = true warningsAsErrors = true checkDependencies = true }}本地测试和设备测试分别覆盖不同层次:
.\gradlew.bat testDebugUnitTest.\gradlew.bat connectedDebugAndroidTest.\gradlew.bat lintDebug.\gradlew.bat check建议为不同变体建立最低验证集:
debug:快速编译、本地单元测试和基础 Lint。staging:接近生产配置的集成测试、网络和数据验证。release:R8、资源收缩、签名、AAB 生成和发布前验证。- 关键 Flavor:至少验证每个独立产品能力和安装 ID。
如果 Lint 使用 Baseline,应定期清理历史问题,避免 Baseline 变成永久忽略清单。
29.19 Android Components 与变体 API
当项目需要对大量变体统一配置时,应优先使用 Android Components API,而不是在配置阶段遍历所有任务并读取内部实现类:
androidComponents { beforeVariants(selector().withBuildType("release")) { variantBuilder -> variantBuilder.enable = true }
onVariants(selector().withBuildType("release")) { variant -> val variantName = variant.name logger.lifecycle("Configuring Android variant: $variantName") }}使用变体 API 时注意:
- API 类型和可用属性会随 AGP 版本变化,升级时需要查阅对应文档。
- 不要依赖
com.android.build.gradle.internal等内部包。 - 只配置真正需要的变体,避免扩大任务图。
- 把变体名称、构建类型和 Flavor 的输入转换为明确的 Provider 或任务输入。
- 如果只是修改 DSL 中已有属性,优先使用公开的
android {}配置。
29.20 自定义任务与生成代码
Android 项目经常需要生成资源、版本信息、API 清单或测试数据。自定义任务应声明输入输出并使用配置避免 API:
abstract class GenerateBuildInfo : DefaultTask() { @get:Input abstract val versionName: Property<String>
@get:OutputFile abstract val outputFile: RegularFileProperty
@TaskAction fun generate() { val file = outputFile.get().asFile file.parentFile.mkdirs() file.writeText("version=${versionName.get()}\n") }}
val generateBuildInfo = tasks.register<GenerateBuildInfo>("generateBuildInfo") { versionName.set(provider { project.version.toString() }) outputFile.set(layout.buildDirectory.file("generated/build-info.txt"))}实际接入 Android Source Set 时,需要根据 AGP 公开 API 或对应版本文档把生成目录添加到正确的变体输入。不要在配置阶段直接写入 src/main,否则容易污染源码、破坏增量构建并引入并发问题。
29.21 Android 构建性能
Android 构建慢时,先区分以下阶段:
Gradle 配置 → AGP 变体计算 → Kotlin/Java 编译 → 资源处理与合并 → Manifest 合并 → DEX/R8 → APK/AAB 打包 → 设备安装和测试常用诊断命令:
.\gradlew.bat assembleDebug --scan.\gradlew.bat assembleDebug --profile.\gradlew.bat assembleDebug --configuration-cache优化原则:
- 减少不必要的 Flavor 和 Build Type 组合。
- 避免每次构建都运行完整 Release R8。
- 使用增量任务、构建缓存和配置缓存时确认插件兼容性。
- 不在配置阶段读取文件、扫描网络或创建大量任务。
- 只为需要的模块启用 Compose、View Binding、BuildConfig 等特性。
- 将设备测试和慢速集成测试从本地快速反馈路径中分离。
- 在 CI 使用稳定的 Gradle User Home 和远程缓存策略,同时避免跨分支污染。
性能优化必须以构建扫描、Profile 报告和任务输入输出为依据,不要仅凭体感关闭校验或减少测试。
29.22 CI 构建矩阵
Android CI 通常需要同时验证编译、测试、静态分析和交付产物:
Pull Request ├── debug 编译 ├── JVM 单元测试 ├── Lint └── 关键模块检查
主分支 ├── staging 变体集成测试 ├── connected tests ├── R8/Release 验证 └── APK/AAB 产物归档
受保护 Tag ├── Release 签名 ├── AAB/APK 校验 ├── SBOM 与依赖报告 └── 应用商店或内部渠道发布CI 环境应固定:
- Gradle Wrapper 和 AGP 版本。
- JDK、Android SDK Platform、Build Tools 和 NDK(若使用)。
- 模拟器镜像或设备测试服务版本。
- 仓库凭据、签名密钥和发布权限。
- 构建缓存与依赖缓存策略。
每个交付构建都应保存 Commit、变体、版本号、签名指纹、产物哈希、Mapping 文件和测试报告。
29.23 Android 常见排错
问题一:AGP 与 Gradle 或 JDK 不兼容
读取 Wrapper、AGP 和实际 JDK 版本,对照官方兼容表逐项核对。不要只升级其中一个版本后反复尝试。
问题二:找不到 assembleXxx 任务
检查 Flavor、Build Type 的实际名称和模块路径:
.\gradlew.bat :app:tasks --all任务名按变体名称使用 Camel Case,例如 freeDebug 对应 assembleFreeDebug。
问题三:Flavor 或变体匹配失败
确认所有 Flavor 都有 dimension,检查 App 与 Library 的 Flavor 名称、Build Type 和 matchingFallbacks,并查看变体属性报告。
问题四:Manifest 合并失败
打开 Manifest Merger 报告,定位冲突来源后使用最小范围的 tools:replace、tools:remove 或 tools:node。不要无差别覆盖权限和组件属性。
问题五:R8 后运行崩溃
根据 Mapping 文件还原堆栈,定位反射、序列化、JNI 或动态加载入口,添加最小 keep 规则并补充 Release 测试。
问题六:设备测试找不到设备
检查 adb devices、模拟器状态、USB 调试、设备 API Level、测试 APK 安装权限和 CI 设备服务配置。
问题七:本地能构建,CI 找不到 SDK
确认 CI 镜像安装了目标 compileSdk、Build Tools、Platform Tools 和必要的命令行工具,避免依赖开发者机器的 local.properties。
问题八:Release 与 Debug 行为不一致
比较实际变体的 Manifest、BuildConfig、资源、依赖、签名、R8 规则和网络配置,不要只比较源代码目录。
29.24 完整 App 模块示例
下面示例展示 App 模块中的常见配置边界。版本、URL、签名和业务值应由实际项目提供:
plugins { id("com.android.application") id("org.jetbrains.kotlin.android")}
android { namespace = "com.example.app" compileSdk = 36
defaultConfig { applicationId = "com.example.app" minSdk = 26 targetSdk = 36 versionCode = 42 versionName = "2.4.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "API_ENV", "\"production\"") }
buildFeatures { buildConfig = true viewBinding = true }
buildTypes { debug { applicationIdSuffix = ".debug" versionNameSuffix = "-debug" isDebuggable = true buildConfigField("String", "API_ENV", "\"development\"") }
release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } }
flavorDimensions += "tier" productFlavors { create("free") { dimension = "tier" applicationIdSuffix = ".free" } create("paid") { dimension = "tier" applicationIdSuffix = ".paid" } }
compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 }}
dependencies { implementation(project(":core")) implementation(libs.androidx.core) testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.espresso.core)}推荐验证:
.\gradlew.bat :app:assembleFreeDebug.\gradlew.bat :app:testFreeDebugUnitTest.\gradlew.bat :app:lintFreeDebug.\gradlew.bat :app:assemblePaidRelease.\gradlew.bat :app:bundlePaidRelease29.25 Android 构建检查清单
环境与模块:
- Gradle Wrapper、AGP、JDK、Kotlin 和 Android Studio 版本兼容。
-
compileSdk、targetSdk、minSdk和 Build Tools 经过明确选择。 - App 与 Library 模块职责清晰,
namespace唯一稳定。 - 仓库集中声明,未在子项目中偷偷增加来源。
变体与输入:
- Build Type 和 Product Flavor 命名清晰,Flavor dimension 完整。
- 变体数量经过评估,没有不必要的笛卡尔积。
- Source Set 目录和覆盖优先级符合预期。
- App 与 Library 的变体可以正确匹配或配置 fallback。
- Manifest、资源、BuildConfig 和环境配置没有泄露 Secret。
质量与交付:
- Debug、Staging、Release 分别有明确验证任务。
- Lint、JVM 测试和设备测试均纳入 CI。
- R8 规则、Mapping 文件和资源收缩结果经过 Release 验证。
- Release APK/AAB 使用受保护签名并保存签名指纹。
- 构建产物、哈希、SBOM、测试报告和 Commit 可以关联。
- CI 使用固定 SDK/JDK 环境并控制发布 Secret 权限。
30. 自定义 Task 与插件
当项目从单模块脚本发展为多模块、跨团队或需要统一工程规范时,直接复制 build.gradle.kts 片段会逐渐失控。自定义 Task 和插件的价值,不是把所有逻辑隐藏起来,而是把重复的构建行为建模为可配置、可测试、可复用的构建能力。
可以把构建逻辑分成三个层次:
任务类型(Task Type) -> 描述一个可复用的动作、输入、输出和执行策略
插件(Plugin) -> 组合任务、扩展、依赖和约定,提供项目级能力
Convention Plugin -> 将组织级标准封装为可按需应用的构建约定本章重点讨论:
- 如何定义正确的 Task 输入和输出。
- 如何使用 Provider API 延迟配置并保持配置缓存兼容。
- 如何处理外部进程、并行工作和共享资源。
- 如何选择脚本插件、预编译脚本插件和二进制插件。
- 如何使用
build-logic、buildSrc和独立插件项目。 - 如何测试、验证、发布和升级自定义插件。
30.1 何时写自定义 Task
适合写 Task 的场景:
- 生成源代码、资源、版本信息或配置文件。
- 校验仓库约定、目录结构、文件格式或依赖清单。
- 调用外部代码生成器、编译器、打包器或测试工具。
- 生成报告、SBOM、校验和或迁移文件。
- 在已有插件任务之间建立清晰的输入输出关系。
- 将多个任务编排为一个明确的生命周期入口。
不适合写 Task 的场景:
- 把业务逻辑、Web 服务或复杂部署系统全部塞进构建脚本。
- 用一个
doLastTask 替代本来应该声明的依赖和输入输出。 - 为了少写几行配置而引入一个难以测试的通用框架。
- 在配置阶段执行网络请求、读取 Secret 或修改源码目录。
- 通过字符串拼接命令隐藏平台差异和失败行为。
判断标准很简单:如果一个动作有稳定的输入、输出、缓存边界和失败语义,它通常适合建模为 Task;如果它需要复杂状态管理、长时间运行或独立权限体系,应考虑放到构建系统之外。
30.2 Task 的生命周期
Task 的生命周期可以分为配置阶段和执行阶段:
配置阶段 -> 创建或注册 Task -> 设置 Provider、依赖和约定 -> 构建任务图
执行阶段 -> 判断是否应执行、跳过或从缓存恢复 -> 准备输入和输出 -> 执行 @TaskAction -> 记录结果、缓存输出和报告推荐使用配置避免 API:
val generateReport = tasks.register("generateReport") { group = "verification" description = "Generates the project verification report." doLast { logger.lifecycle("Generating report") }}
tasks.named("check") { dependsOn(generateReport)}与 tasks.create 相比,tasks.register 不会立即创建并配置任务对象。只有任务真正参与任务图时,Gradle 才会按需完成配置。
常用 API:
| API | 适用场景 |
|---|---|
tasks.register | 注册新任务,推荐默认使用 |
tasks.named | 延迟获取已存在任务 |
tasks.withType<T>().configureEach | 为同类型任务统一配置 |
tasks.configureEach | 对所有现有和未来任务执行延迟配置 |
tasks.create | 兼容特殊场景,通常不作为默认选择 |
tasks.getByName | 立即获取任务,可能破坏配置阶段性能 |
30.3 自定义 Task 类型
Task 类型应使用抽象属性描述输入和输出,让 Gradle 能够理解任务模型:
import org.gradle.api.DefaultTaskimport org.gradle.api.file.RegularFilePropertyimport org.gradle.api.provider.Propertyimport org.gradle.api.tasks.Inputimport org.gradle.api.tasks.OutputFileimport org.gradle.api.tasks.TaskAction
abstract class GenerateVersionFile : DefaultTask() { @get:Input abstract val version: Property<String>
@get:OutputFile abstract val destination: RegularFileProperty
@TaskAction fun writeFile() { val file = destination.get().asFile file.parentFile.mkdirs() file.writeText("version=${version.get()}\n") }}
tasks.register<GenerateVersionFile>("generateVersionFile") { group = "build setup" description = "Generates a version properties file." version.set(provider { project.version.toString() }) destination.set(layout.buildDirectory.file("generated/version.properties"))}这个定义表达了:
version是任务输入,版本变化会使任务重新执行。destination是任务输出,Gradle 可以判断输出是否存在和是否过期。- 任务不在配置阶段写文件,实际写入发生在
@TaskAction。 - 输出位于
build目录,不会污染源码目录。
30.4 输入属性注解
常用输入注解如下:
| 注解 | 含义 |
|---|---|
@Input | 字符串、数字、布尔值、枚举等简单值 |
@InputFile | 单个输入文件 |
@InputFiles | 多个输入文件或文件集合 |
@InputDirectory | 输入目录 |
@Classpath | 影响类路径和类加载结果的文件集合 |
@CompileClasspath | 编译类路径,通常忽略不影响 ABI 的内容 |
@Nested | 嵌套的输入对象 |
@Optional | 输入允许缺失 |
@Internal | 不参与增量、缓存和 up-to-date 判断 |
@OutputFile | 单个输出文件 |
@OutputDirectory | 输出目录 |
@Destroys | 任务会删除或破坏的目录 |
@LocalState | 仅本地保留、不参与远程缓存的状态 |
文件输入通常还需要声明路径敏感性:
import org.gradle.api.file.DirectoryPropertyimport org.gradle.api.tasks.InputDirectoryimport org.gradle.api.tasks.OutputDirectoryimport org.gradle.api.tasks.PathSensitiveimport org.gradle.api.tasks.PathSensitivity
abstract class CopyGeneratedResources : DefaultTask() { @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sourceDirectory: DirectoryProperty
@get:OutputDirectory abstract val outputDirectory: DirectoryProperty
@TaskAction fun copy() { project.copy { from(sourceDirectory) into(outputDirectory) } }}PathSensitivity.RELATIVE 表示相对于输入根目录的路径参与判断,避免绝对工作区路径导致不同机器无法复用缓存。具体敏感性应根据任务语义选择,而不是机械套用。
30.5 输入输出必须完整
任务缓存和增量执行正确与否,取决于所有影响结果的输入是否被声明。隐式输入包括:
- 环境变量和系统属性。
- 外部工具版本。
- 配置文件、模板和规则文件。
- 当前 Git Commit 或分支信息。
- Java、Kotlin、Node 等工具链版本。
- 网络响应、数据库状态或当前时间。
如果任务读取了某个文件却没有声明为输入,文件变化后 Gradle 可能错误地跳过任务或复用旧缓存。相反,把无关的时间戳、绝对路径或临时目录声明为输入,会降低复用率。
无法可靠声明为稳定输入的内容,不应直接作为可缓存任务的输入。可以:
- 将网络请求移出普通 Task,先生成固定输入文件。
- 将工具版本、参数和环境显式建模为
@Input。 - 对当前时间等非确定性内容关闭缓存或改为由上游任务生成。
- 使用 Build Service 管理有限的外部资源。
- 为外部系统建立独立的同步或准备阶段。
30.6 Provider API 与延迟配置
Provider API 用于表示“稍后才能确定的值”:
val releaseVersion = providers.gradleProperty("releaseVersion") .orElse("0.1.0-SNAPSHOT")
val versionFile = tasks.register<GenerateVersionFile>("generateVersionFile") { version.set(releaseVersion) destination.set(layout.buildDirectory.file("generated/version.properties"))}常用方法:
map:将一个 Provider 转换成另一个 Provider。flatMap:根据 Provider 结果继续获得另一个 Provider。orElse:提供默认值。zip:组合多个 Provider。forUseAtConfigurationTime:旧版本兼容场景,新的构建逻辑应优先避免配置期读取。
不要在配置阶段过早调用 .get():
// 不推荐:配置阶段立即读取val version = providers.gradleProperty("releaseVersion").get()
// 推荐:把 Provider 传给任务属性versionFile.configure { version.set(providers.gradleProperty("releaseVersion").orElse("0.1.0"))}只有在任务执行动作内部或确实需要最终值的边界处,才读取 Property 或 Provider 的值。
30.7 Task 之间连接输入输出
不要只用 dependsOn 表达文件关系:
val generatedVersion = tasks.register<GenerateVersionFile>("generatedVersion") { version.set(provider { project.version.toString() }) destination.set(layout.buildDirectory.file("generated/version.properties"))}
tasks.named<ProcessResources>("processResources") { from(generatedVersion.map { it.destination })}当一个任务的输出作为另一个任务的输入时,Gradle 可以自动推断任务依赖。只有在存在顺序关系但没有文件输入输出关系时,才需要显式使用 dependsOn:
tasks.named("publish") { dependsOn("verifyReleaseMetadata")}区别如下:
from(taskProvider.map { it.output }):表达数据流,通常是首选。dependsOn(taskProvider):表达执行顺序,但不说明数据如何流动。mustRunAfter:只约束顺序,不自动触发另一个任务。shouldRunAfter:尽量遵守顺序,存在冲突时可以忽略。
30.8 文件 API 与临时目录
使用 Gradle 的文件 API 管理路径:
abstract class GenerateManifest : DefaultTask() { @get:InputFile abstract val template: RegularFileProperty
@get:OutputFile abstract val output: RegularFileProperty
@TaskAction fun generate() { val content = template.get().asFile.readText() output.get().asFile.apply { parentFile.mkdirs() writeText(content.replace("@VERSION@", project.version.toString())) } }}推荐:
- 输出统一放在
layout.buildDirectory下。 - 用
DirectoryProperty、RegularFileProperty和ConfigurableFileCollection表达路径。 - 让目录创建发生在执行阶段。
- 使用
fileTree或sourceSets的 Provider,而不是配置阶段遍历全部文件。 - 临时文件使用
temporaryDir,不要写入项目根目录。
不要在任务中使用固定的系统临时目录名称,也不要让多个并行任务共享同一个可写临时目录。
30.9 外部进程
外部工具应该在执行阶段运行,并且工具位置、参数、输入和输出应明确建模:
import org.gradle.api.file.RegularFilePropertyimport org.gradle.api.provider.Propertyimport org.gradle.api.tasks.Inputimport org.gradle.api.tasks.InputFileimport org.gradle.api.tasks.OutputFileimport org.gradle.api.tasks.TaskActionimport org.gradle.process.ExecOperationsimport javax.inject.Inject
abstract class RunCodeGenerator @Inject constructor( private val execOperations: ExecOperations) : DefaultTask() { @get:Input abstract val toolVersion: Property<String>
@get:InputFile abstract val schema: RegularFileProperty
@get:OutputFile abstract val generatedSource: RegularFileProperty
@TaskAction fun run() { execOperations.exec { commandLine( "java", "-jar", "tools/code-generator-${toolVersion.get()}.jar", schema.get().asFile.absolutePath, generatedSource.get().asFile.absolutePath ) } }}真实项目还应考虑:
- 使用 Toolchain 或明确的工具安装目录,避免依赖 PATH 偶然命中。
- 通过
ExecOperations而不是在配置阶段调用Runtime.exec。 - 将工具版本、命令参数和环境差异作为任务输入。
- 处理退出码、标准输出、标准错误和超时。
- 为 Windows、Linux、macOS 提供不同的可执行文件或统一包装层。
- 不把 Secret 拼接进命令行,因为命令行可能出现在进程列表和日志中。
30.10 Worker API
当一个 Task 需要对大量互相独立的输入执行相同工作时,可以使用 Worker API:
import org.gradle.workers.WorkActionimport org.gradle.workers.WorkParametersimport org.gradle.workers.WorkerExecutorimport javax.inject.Inject
interface HashParameters : WorkParameters { val input: RegularFileProperty val output: RegularFileProperty}
abstract class HashWorkAction : WorkAction<HashParameters> { override fun execute() { val hash = parameters.input.get().asFile .inputStream() .use { stream -> stream.readBytes().contentHashCode() } parameters.output.get().asFile.apply { parentFile.mkdirs() writeText(hash.toString()) } }}
abstract class HashFilesTask : DefaultTask() { @get:Inject abstract val workerExecutor: WorkerExecutor
@TaskAction fun hashFiles() { // 真实项目应为每个输入注册一个独立 WorkAction。 workerExecutor.noIsolation().submit(HashWorkAction::class.java) { // 配置 input 和 output Provider } }}Worker API 的隔离模式:
noIsolation:工作单元共享进程,开销小,但必须自行保证线程安全。classLoaderIsolation:使用独立 ClassLoader,适合不同工具类路径。processIsolation:使用独立进程,隔离性更强但开销更高。
使用 Worker API 时要确保:
- 每个工作单元的输入输出互不冲突。
- 工作单元不修改共享全局状态。
- 并行度受到 CPU、内存和外部工具能力约束。
- 失败能够被 Gradle 正确报告,而不是被吞掉。
- 任务整体的输入输出仍然完整声明。
30.11 Build Service
Build Service 适合管理多个 Task 共享的资源,例如:
- HTTP 连接池或 API 客户端。
- 许可证服务或限流器。
- 外部工具守护进程。
- 统计构建任务执行次数的监听器。
- 需要限制并发访问的共享文件或设备。
基本结构:
abstract class ApiClientService : BuildService<ApiClientService.Parameters>, AutoCloseable { interface Parameters : BuildServiceParameters { val endpoint: Property<String> }
override fun close() { // 关闭连接池、文件句柄或外部进程 }
fun fetch(path: String): String { return "${parameters.endpoint.get()}$path" }}
val apiClient = gradle.sharedServices.registerIfAbsent( "apiClient", ApiClientService::class) { parameters.endpoint.set( providers.gradleProperty("apiEndpoint").orElse("https://api.example.com") ) maxParallelUsages.set(2)}使用 Build Service 的任务应声明使用关系:
tasks.withType<GenerateManifest>().configureEach { usesService(apiClient)}Build Service 不是普通的全局单例。它由 Gradle 管理生命周期,可以配置最大并发使用数,并在构建结束时释放资源。不要把任意可变项目状态塞进 Service 中,也不要用它绕过任务输入输出声明。
30.12 可缓存 Task 与不可缓存 Task
纯函数式、输入输出完整且结果稳定的 Task 可以标记为可缓存:
import org.gradle.api.tasks.CacheableTask
@CacheableTaskabstract class GenerateReport : DefaultTask() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) abstract val inputFiles: ConfigurableFileCollection
@get:OutputFile abstract val report: RegularFileProperty
@TaskAction fun generate() { val result = inputFiles.files .sortedBy { it.invariantSeparatorsPath } .joinToString("\n") { it.name } report.get().asFile.apply { parentFile.mkdirs() writeText(result) } }}以下任务通常不应直接启用缓存:
- 依赖当前时间、随机数或不可复现网络响应。
- 读取未声明的数据库、设备或环境状态。
- 输出包含机器专属路径、用户名或 Secret。
- 会修改外部系统、发布构件或发送通知。
- 任务结果无法通过输入完整描述。
不确定时,可以使用 @DisableCachingByDefault 并先补齐模型。错误缓存比不缓存更危险,因为它可能把错误结果传播到多个开发者和 CI。
30.13 Task 依赖与生命周期编排
自定义 Task 应接入已有生命周期,而不是让开发者记住一串独立命令:
val verifyApi = tasks.register("verifyApi") { group = "verification" description = "Checks the generated API contract."}
tasks.named("check") { dependsOn(verifyApi)}如果任务只对某个组件生效,应配置对应任务类型:
tasks.withType<Test>().configureEach { useJUnitPlatform()}避免把任务无条件挂到 build:
- 需要发布时才运行的校验,应挂到发布前任务或独立生命周期。
- 耗时的设备测试不应默认阻塞所有本地
build。 - 生成源码任务应连接到对应 Source Set 或编译任务。
- 质量门禁应挂到
check,让团队有统一入口。
30.14 插件的基本形式
插件负责把能力应用到 Project:
import org.gradle.api.Pluginimport org.gradle.api.Projectimport org.gradle.api.plugins.JavaPluginExtensionimport org.gradle.jvm.toolchain.JavaLanguageVersion
class CompanyJavaConventions : Plugin<Project> { override fun apply(project: Project) { project.pluginManager.apply("java-library")
project.extensions.configure<JavaPluginExtension> { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) }
project.tasks.withType<Test>().configureEach { useJUnitPlatform() } }}插件应遵循以下边界:
- 只配置自己声明或应用的能力。
- 使用公开 API、Provider 和类型安全扩展。
- 不修改其他插件的内部对象或内部包。
- 不假定所有项目都有固定名称的任务和 Configuration。
- 通过
pluginManager.withPlugin响应其他插件是否被应用。 - 让项目可以通过扩展属性覆盖合理的默认值。
30.15 插件扩展与可配置 DSL
插件可以创建扩展,为使用者提供稳定的配置入口:
abstract class CompanyConventionsExtension { abstract val javaVersion: Property<Int> abstract val enablePublishing: Property<Boolean>}
class CompanyJavaPlugin : Plugin<Project> { override fun apply(project: Project) { val extension = project.extensions.create<CompanyConventionsExtension>( "companyConventions" ) extension.javaVersion.convention(21) extension.enablePublishing.convention(false)
project.pluginManager.apply("java-library") project.pluginManager.withPlugin("maven-publish") { project.logger.lifecycle("Maven publishing is enabled") } }}使用插件:
plugins { id("com.example.java-conventions")}
companyConventions { javaVersion.set(21) enablePublishing.set(true)}扩展设计建议:
- 属性使用
Property<T>、ListProperty<T>或SetProperty<T>。 - 使用
convention提供默认值,不要在插件中强制覆盖使用者配置。 - 公开稳定、语义化的属性,隐藏任务和实现细节。
- 对非法组合尽早报错,并给出可行动的错误信息。
- 变更扩展属性时考虑插件版本兼容性和迁移路径。
30.16 withPlugin 与插件应用顺序
插件之间经常存在可选协作关系:
pluginManager.withPlugin("java") { extensions.configure<JavaPluginExtension> { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) }
tasks.withType<Test>().configureEach { useJUnitPlatform() }}
pluginManager.withPlugin("com.android.application") { logger.lifecycle("Configure Android application conventions")}withPlugin 的好处是:
- 不需要假设目标插件一定已应用。
- 无论目标插件先应用还是后应用,都能执行配置。
- 避免直接访问不存在的扩展和任务。
- 让 Convention Plugin 可以兼容多个项目类型。
不要使用 afterEvaluate 作为默认解决方案。它会延迟错误、破坏配置缓存,并且使插件之间的配置顺序更难理解。
30.17 脚本插件、预编译脚本与二进制插件
Gradle 插件实现方式大致分为三类:
| 类型 | 适用场景 | 优点 | 风险 |
|---|---|---|---|
| 脚本插件 | 小型局部复用 | 上手快、文件直观 | 类型安全弱、依赖和测试能力有限 |
| 预编译脚本插件 | build-logic 中的组织级约定 | Kotlin DSL、可复用、易迁移 | 需要独立构建逻辑工程 |
| 二进制插件 | 跨仓库、需要版本化和发布的能力 | API 稳定、可测试、可发布 | 开发和发布成本更高 |
脚本插件示例:
tasks.withType<Test>().configureEach { useJUnitPlatform()}apply(from = "gradle/java-quality.gradle.kts")脚本插件适合过渡和很小的复用。随着逻辑增加,建议迁移到预编译脚本或二进制插件。
30.18 buildSrc 与 build-logic
buildSrc 是 Gradle 自动识别的特殊构建逻辑目录,迁移成本低,但它的变化可能导致整个构建逻辑重新编译和配置。大型项目更推荐使用独立的 Included Build:
build-logic/├── settings.gradle.kts├── build.gradle.kts└── src/main/kotlin/ ├── company.java-base.gradle.kts └── company.java-library.gradle.kts根 settings.gradle.kts:
pluginManagement { includeBuild("build-logic")}build-logic/build.gradle.kts:
plugins { `kotlin-dsl`}
repositories { gradlePluginPortal() mavenCentral()}选择建议:
- 小型单仓库、逻辑很少:可以先使用
buildSrc。 - 多模块组织级规则:优先使用
build-logicConvention Plugin。 - 多仓库共享且需要独立发布:使用独立插件工程。
- 不要把业务源码和构建逻辑混在同一个普通模块中。
30.19 Convention Plugin 示例
build-logic/src/main/kotlin/company.java-library.gradle.kts:
plugins { `java-library` id("company.java-base")}
java { withSourcesJar() withJavadocJar()}
tasks.withType<Test>().configureEach { useJUnitPlatform()}
tasks.withType<Javadoc>().configureEach { options.encoding = "UTF-8"}项目模块只需要应用约定:
plugins { id("company.java-library")}
dependencies { api(project(":api")) implementation(libs.jackson.databind)}Convention Plugin 的职责应该是统一约定,而不是消灭所有差异。模块特有的坐标、额外构件和特殊测试仍然应该保留在模块脚本中。
30.20 二进制插件工程
二进制插件可以使用 java-gradle-plugin:
plugins { `java-gradle-plugin` `kotlin-dsl` `maven-publish`}
group = "com.example.build"version = "1.0.0"
gradlePlugin { plugins { create("companyJava") { id = "com.example.java-conventions" implementationClass = "com.example.build.CompanyJavaPlugin" displayName = "Example Java conventions" description = "Shared Java build conventions for Example projects." } }}插件 ID 应该稳定、具有组织边界且不与其他插件冲突。实现类应该放在明确的包中,并通过正常的 Java/Kotlin 依赖管理获取所需库。
30.21 插件验证与 validatePlugins
插件开发阶段应运行 Gradle 的插件验证:
.\gradlew.bat validatePlugins验证重点包括:
- Task 属性是否正确声明输入和输出。
- 是否存在不支持配置缓存或缓存的 API 使用。
- 是否使用了不安全的类型或内部实现。
- 是否存在无法被 Gradle 理解的 Provider 或文件属性。
- 自定义 Task 是否缺少注解或类型信息。
将验证接入插件工程的 check:
tasks.named("check") { dependsOn("validatePlugins")}验证警告不应被简单压制。每个忽略项都需要说明原因、影响和后续修复计划。
30.22 插件单元测试与 TestKit
可以使用 ProjectBuilder 测试插件是否正确创建扩展、任务和默认值:
class CompanyJavaPluginTest { @Test fun `plugin creates extension`() { val project = ProjectBuilder.builder().build()
project.pluginManager.apply("com.example.java-conventions")
assertNotNull(project.extensions.findByName("companyConventions")) }}单元测试适合验证:
- 扩展默认值和属性校验。
- 插件应用后的任务、Configuration 和依赖。
- 不同插件组合下的配置分支。
- 任务类型的输入输出和行为。
但 ProjectBuilder 不能完全模拟真实 Gradle 命令行。需要验证完整构建时,应使用 Gradle TestKit:
class PluginFunctionalTest { @Test fun `plugin runs in a real build`() { val result = GradleRunner.create() .withProjectDir(testProjectDir) .withPluginClasspath() .withArguments("companyTask", "--stacktrace") .forwardOutput() .build()
assertTrue(result.output.contains("BUILD SUCCESSFUL")) }}Functional Test 应覆盖:
- 最小项目应用插件。
- Java、Kotlin、Android 等目标插件的组合。
- Windows、Linux 或其他受支持平台差异。
- 增量执行、缓存命中和配置缓存。
- 任务失败时的错误信息。
- 插件升级时的兼容行为。
30.23 插件测试项目布局
一个可维护的插件工程可以按以下方式组织:
build-logic/├── build.gradle.kts├── settings.gradle.kts├── src/main/kotlin/│ └── com/example/build/CompanyJavaPlugin.kt├── src/test/kotlin/│ └── com/example/build/CompanyJavaPluginTest.kt└── src/functionalTest/kotlin/ └── com/example/build/CompanyJavaPluginFunctionalTest.kt功能测试项目通常放在临时目录中,由测试代码生成:
functional-test-project/├── settings.gradle.kts├── build.gradle.kts└── src/main/java/测试应使用固定、最小的输入,避免依赖开发者机器上安装的插件、JDK 或本地仓库。需要外部依赖时,应使用测试仓库、固定版本和可控缓存。
30.24 插件发布与版本化
插件发布可以面向 Plugin Portal、内部 Maven 仓库或组织自己的插件仓库。发布前应验证:
- Plugin Marker Artifact 坐标正确。
- 插件 ID、实现类和版本元数据一致。
- POM、Gradle Module Metadata、源码和文档齐全。
- 插件没有把开发环境绝对路径写入产物。
- 插件所需 Gradle、JDK 和 AGP 版本在文档中明确。
- Release 版本不可覆盖,签名和校验文件完整。
Plugin Portal 发布前可以执行验证任务:
.\gradlew.bat publishPlugins --validate-only内部发布使用 maven-publish 时,应把插件工程当作普通发布项目治理:
publishing { repositories { maven { name = "internal" url = uri("https://repo.example.com/gradle-plugins") credentials(PasswordCredentials::class) } }}插件版本应遵循语义化版本或组织统一版本策略。破坏扩展 DSL、任务名称、默认行为或兼容版本范围时,应该提升主版本或提供迁移说明。
30.25 插件兼容性与升级策略
插件兼容性至少包括:
- Gradle 版本。
- Java 运行时和 Toolchain 版本。
- 目标语言插件,如 Java、Kotlin、Android。
- 操作系统和外部工具。
- Configuration Cache、Build Cache 和并行执行能力。
- 使用者项目的多项目结构和插件应用顺序。
不要在插件中读取 Gradle 内部类:
// 不推荐:依赖内部实现// import org.gradle.internal.some.InternalType升级插件前应:
- 使用 TestKit 在支持的 Gradle 版本矩阵中运行功能测试。
- 检查弃用警告和
validatePlugins输出。 - 验证配置缓存、增量执行和远程缓存行为。
- 检查生成的构件、依赖和任务名称是否发生破坏性变化。
- 更新插件 README、迁移指南和兼容性声明。
30.26 插件性能与安全
插件在配置阶段运行于所有应用它的项目中,因此一个低效操作可能被放大到整个多项目构建:
- 使用
register、named和configureEach延迟配置。 - 避免对所有文件执行配置期扫描。
- 不要在
apply中访问网络或启动外部进程。 - 使用 Provider、Build Service 和 Worker API 管理延迟资源。
- 不要为每个模块重复创建相同的共享服务或工具进程。
- 任务日志默认保持简洁,详细信息通过
--info或专门报告输出。
插件也是构建供应链的一部分:
- 发布到受信任仓库并启用签名和验证。
- 不把 Secret 写入扩展、任务输出或构建扫描。
- 不执行从网络下载后未经校验的脚本。
- 将插件依赖、Wrapper 和构建逻辑纳入锁定与依赖验证。
- 对插件升级进行代码审查和功能测试。
30.27 完整 Convention Plugin 示例
下面展示一个小型 build-logic Convention Plugin 的主要结构:
build-logic/├── settings.gradle.kts├── build.gradle.kts└── src/main/kotlin/ ├── company.java-base.gradle.kts └── company.java-library.gradle.ktsplugins { `kotlin-dsl`}
repositories { gradlePluginPortal() mavenCentral()}plugins { java}
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
tasks.withType<Test>().configureEach { useJUnitPlatform()}
tasks.withType<JavaCompile>().configureEach { options.encoding = "UTF-8"}plugins { id("company.java-base") `java-library` `maven-publish`}
java { withSourcesJar() withJavadocJar()}
group = "com.example.libs"
publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) } }}根项目只应用约定:
pluginManagement { includeBuild("build-logic")}plugins { id("company.java-library")}
dependencies { api(project(":api")) implementation(libs.jackson.databind)}这个结构把共享策略集中在 build-logic,但保留每个模块的业务依赖和特殊配置,适合逐步迁移现有的大型根脚本。
30.28 自定义 Task 与插件检查清单
Task 建模:
- 使用抽象类型和 Provider 属性描述输入输出。
- 所有影响结果的文件、参数、工具版本和环境都已建模。
- 输出位于
build或明确的构建目录。 -
@TaskAction只负责执行,不在配置阶段产生副作用。 - 文件路径声明了合适的 Path Sensitivity。
- 可缓存任务具有稳定、完整、可复现的输入输出。
执行与并发:
- 外部进程通过
ExecOperations在执行阶段运行。 - 大量独立工作使用 Worker API,并控制隔离模式和并发度。
- 共享连接、工具进程或限流器使用 Build Service。
- 任务依赖优先通过输入输出连接表达。
- 不使用全局可变状态、固定临时目录或未声明网络输入。
插件工程:
- 共享逻辑优先使用
build-logicConvention Plugin。 - 扩展使用类型安全的
Property,默认值使用convention。 - 插件通过
withPlugin适配可选目标插件。 - 不依赖 Gradle 或 AGP 内部 API。
-
validatePlugins、单元测试和 TestKit 功能测试纳入check。 - 插件 ID、版本、兼容矩阵和迁移说明清晰。
- 发布构件、签名、校验和依赖元数据完整。
31. 常见错误与系统化排查
Gradle 错误排查的难点通常不在于错误信息不存在,而在于错误发生在不同层级:Wrapper、JVM、Settings、插件解析、依赖解析、变体匹配、任务图、任务执行、缓存、外部工具或 CI 环境。只在最后一行异常上反复尝试,容易把真正原因隐藏起来。
本章采用“建立基线—收集证据—缩小范围—验证假设—记录修复”的流程。目标不是让某一次构建偶然成功,而是找到能够解释问题、可以复现并且不会破坏其他变体和环境的根因。
31.1 排查的第一原则
遇到构建失败时,先回答五个问题:
- 失败发生在初始化、配置还是执行阶段?
- 失败是稳定复现,还是只在某个环境、分支或缓存状态出现?
- 最近改变了什么:代码、Wrapper、JDK、插件、依赖、仓库、构建参数还是 CI 镜像?
- 最小复现需要哪些模块、任务和依赖?
- 什么证据可以证明修复没有掩盖问题?
建议先记录:
时间:Git Commit:Gradle Wrapper:Gradle JVM:Java Toolchain:操作系统:执行命令:失败任务:首次出现时间:最近变更:是否可在干净目录复现:是否受缓存影响:常见但不可靠的第一反应包括:
- 立即执行
clean,没有确认是否为增量输入问题。 - 随意增加 Maven 仓库,没有确认坐标来源和仓库优先级。
- 使用
force统一所有版本,没有分析冲突和兼容关系。 - 删除锁文件或验证元数据,让构建“先通过再说”。
- 直接升级 Gradle、JDK 和全部插件,导致无法判断哪个变化解决了问题。
- 把 CI 失败归因于“服务器不稳定”,没有保存环境和日志。
31.2 建立可重复的诊断基线
优先使用项目 Wrapper,而不是系统全局 Gradle:
.\gradlew.bat --version.\gradlew.bat help --no-daemonLinux 或 macOS:
./gradlew --version./gradlew help --no-daemon建立基线时可以依次执行:
.\gradlew.bat tasks --all.\gradlew.bat properties.\gradlew.bat projects.\gradlew.bat buildEnvironment这些命令可以确认:
- Wrapper 实际使用的 Gradle 版本和 JVM。
- Settings 识别到的项目和模块。
- 插件、构建逻辑和初始化脚本是否能够加载。
- 目标任务是否存在,以及任务属于哪个项目。
如果 help 都无法执行,不要从业务任务开始排查。先解决 Wrapper、JDK、Settings、插件解析或初始化脚本问题。
31.3 从异常链读取根因
Gradle 输出通常包含多个层次:
FAILURE: Build failed with an exception.
* What went wrong:Execution failed for task ':app:test'.> 原始异常
* Try:> Run with --stacktrace option to get the stack trace.排查顺序建议是:
- 找到第一个失败任务,例如
:app:test或:build-logic:compileKotlin。 - 找到最内层的
Caused by或实际工具错误。 - 区分 Gradle 报告的包装异常和编译器、测试框架、R8、SDK 工具的原始异常。
- 记录失败任务的输入变体、Configuration、文件路径和外部命令。
- 用最小范围任务重新执行,而不是每次运行完整
build。
常用日志参数:
.\gradlew.bat :app:test --stacktrace.\gradlew.bat :app:test --info.\gradlew.bat :app:test --debug.\gradlew.bat :app:test --scan--debug 可能输出凭据相关路径、内部 URL 或大量环境信息。分享日志前应脱敏;涉及私有仓库和 CI Secret 时,优先使用受控的 Build Scan 或内部日志系统。
31.4 Wrapper、安装与 JDK 问题
gradlew 或 gradlew.bat 无法执行
检查:
Get-ChildItem -Force gradlew, gradlew.bat, gradle\wrapperGet-Content gradle\wrapper\gradle-wrapper.propertiesLinux 或 macOS 还要检查执行权限:
ls -l gradlewchmod +x gradlew不要从个人机器复制一个未知来源的 gradle-wrapper.jar。Wrapper 文件应纳入版本控制,并通过组织规定的校验和或验证流程检查。
JAVA_HOME is set to an invalid directory
$env:JAVA_HOMEjava -version.\gradlew.bat --version确认 JAVA_HOME 指向 JDK 根目录,而不是 bin 目录,并确认命令行使用的 java 与 Gradle 实际使用的 JVM 一致。
Unsupported class file major version
通常表示运行某个工具的 JVM 无法理解编译产物,或者 Gradle、插件和 JDK 版本组合不兼容。分别核对:
- 运行 Gradle 的 JVM。
- 编译 Java/Kotlin 的 Toolchain。
- 测试运行时 JVM。
- 应用或插件实际运行的 JVM。
- 依赖构件的字节码版本。
java -version.\gradlew.bat --version.\gradlew.bat dependencies不要只把 sourceCompatibility 改低。首先确认是哪个任务或依赖产生、读取了不兼容的字节码。
31.5 插件解析失败
Plugin ... was not found
检查 settings.gradle.kts 中的 pluginManagement.repositories、插件 ID、版本号和插件 Marker:
pluginManagement { repositories { gradlePluginPortal() mavenCentral() google() }}插件解析仓库与项目依赖仓库是两套概念。项目中声明了 mavenCentral(),不代表插件解析一定能从那里找到对应的 Plugin Marker。
Plugin request for plugin already on the classpath
常见原因:插件既通过 plugins {} 请求,又被 buildscript 或某个构建逻辑提前加入 Classpath。排查:
.\gradlew.bat buildEnvironment.\gradlew.bat :build-logic:dependencies统一插件应用方式,避免同一个插件由根脚本、buildSrc、Included Build 和子项目重复提供。
插件升级后配置项不存在
检查插件版本的 DSL 变化、弃用说明和迁移指南。不要把旧版本 DSL 通过反射或内部 API 强行恢复;如果插件需要按版本兼容,应在 Convention Plugin 中清晰隔离。
31.6 Could not resolve dependency
先确认:
- 坐标、模块名和版本是否正确。
- 仓库是否声明在正确的 Settings 或 Project 位置。
- 仓库内容过滤是否排除了目标 group。
- 网络、代理、证书和 DNS 是否正常。
- 私服凭据是否存在且有读取权限。
- 目标版本是否真的发布,以及是否只发布了特定变体。
- 依赖所在 Configuration 是否正确。
- 锁文件或 Dependency Verification 是否拒绝了新内容。
查看依赖图:
.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencyInsight --dependency example-core --configuration runtimeClasspath刷新依赖只适合验证缓存或远程元数据问题:
.\gradlew.bat build --refresh-dependencies --info不要一看到解析失败就随意增加仓库。新增仓库可能引入错误来源、名称抢注、不同元数据或依赖混淆。先确认目标模块的官方发布仓库和项目集中仓库策略。
31.7 仓库、凭据与内容过滤问题
如果本地能解析而 CI 失败,重点对比:
- 仓库 URL 是否一致。
- CI 是否注入了正确的用户名、Token 和证书。
- Secret 是否只在需要的 Job 中可用。
- 仓库是否要求 HTTP Header、Basic Auth 或其他认证方式。
- CI 是否使用了不同的
GRADLE_USER_HOME、代理或初始化脚本。 exclusiveContent或content.includeGroup是否误过滤了模块。
可以临时输出不敏感的环境摘要:
$env:GRADLE_USER_HOME$env:JAVA_HOME.\gradlew.bat --version不要使用 --debug 直接把带认证请求的完整日志上传到公共 Issue。必要时在内部环境采集,并对 URL、Header、Token 和用户名进行脱敏。
31.8 NoSuchMethodError、NoClassDefFoundError 与二进制冲突
这类错误经常表示编译时和运行时加载了不同版本的类,或者某个可选运行时依赖未被带上:
.\gradlew.bat dependencyInsight --dependency jackson --configuration runtimeClasspath.\gradlew.bat dependencies --configuration compileClasspath.\gradlew.bat dependencies --configuration runtimeClasspath排查顺序:
- 确认缺失类属于哪个模块。
- 对比
compileClasspath与runtimeClasspath。 - 检查版本冲突、平台、BOM 和约束。
- 检查
api、implementation、compileOnly和runtimeOnly作用域。 - 确认库的 Feature Variant 和可选依赖是否被正确选择。
- 用最小运行示例验证修复。
优先使用 BOM、Platform、Constraint 和直接依赖升级治理,不要先用大量 force。强制版本可能让编译通过,却在运行期引入不兼容行为。
31.9 Duplicate class 与重复资源
常见原因:
- 两个依赖带有相同类。
- AndroidX 与旧 Support Library 混用。
- 同一个本地 JAR 被重复加入。
- Fat JAR 把多个归档直接合并。
- 不同变体引入了不兼容实现。
- 资源或服务描述文件没有按规则合并。
.\gradlew.bat :app:dependencies --configuration debugRuntimeClasspath.\gradlew.bat :app:dependencyInsight --dependency some-library --configuration debugRuntimeClasspath不要简单使用 DuplicatesStrategy.EXCLUDE 或删除一个 JAR。先确认重复内容是否等价、哪一个版本应保留,以及 META-INF/services、许可证和资源文件是否需要合并。
31.10 No matching variant
变体匹配失败时,检查生产者和消费者的属性:
- 用例:编译、运行、测试或发布。
Usage:Java API 或 Runtime。Category:Library、Platform 或其他类型。Library Elements:Classes、JAR 或其他构件。- JVM 版本和 Android 属性。
- Flavor、Build Type、能力和自定义属性。
.\gradlew.bat :consumer:dependencies --configuration runtimeClasspath.\gradlew.bat :producer:outgoingVariants常见修复方式:
- 发布方正确声明组件和变体,而不是让消费方使用
files(...)绕过模型。 - 消费方使用正确的 Configuration。
- 为自定义 Build Type 配置
matchingFallbacks。 - 对齐 Flavor Dimension 和属性。
- 检查 Java Toolchain、Android SDK 或插件版本要求。
把依赖改成本地文件只能隐藏变体问题,还会丢失传递依赖、元数据和发布信息。
31.11 任务不存在、未执行或执行顺序错误
任务不存在
先确认任务属于哪个项目以及实际名称:
.\gradlew.bat tasks --all.\gradlew.bat :app:tasks --all多项目任务必须包含正确的项目路径,例如:
.\gradlew.bat :core:test.\gradlew.bat :app:assembleDebug任务被跳过或显示 UP-TO-DATE
查看日志中的跳过原因:
.\gradlew.bat check --info如果需要确认任务本身是否能够执行,可以临时使用:
.\gradlew.bat check --rerun-tasks--rerun-tasks 只用于诊断,不是修复方案。真正的修复是补全任务输入输出、修正依赖关系或消除不稳定的生成步骤。
任务执行顺序错误
不要依赖任务名称排序或手工先后调用。使用任务依赖表达关系:
tasks.named("packageApp") { dependsOn("generateAppMetadata")}如果只是需要确保另一个任务完成但不想复制其输出,区分 dependsOn、mustRunAfter 和 finalizedBy 的语义,不要随意组合。
31.12 Configuration Cache 报错
先生成问题报告:
.\gradlew.bat build --configuration-cache逐条处理报告中的问题:
- 去掉任务动作中的 Project、Settings 或 Gradle 对象引用。
- 使用
Property、Provider、RegularFileProperty和DirectoryProperty传递输入。 - 将文件、环境变量和系统属性声明为任务输入。
- 移除配置阶段 I/O、网络访问和外部进程执行。
- 升级或替换不兼容插件。
- 对确实不能兼容的任务显式声明原因,而不是静默忽略。
可以在迁移期将问题显示为警告:
.\gradlew.bat build ` -Dorg.gradle.configuration-cache=true ` -Dorg.gradle.configuration-cache.problems=warn配置缓存问题应按“存储阶段问题—加载阶段问题—第二次执行复用”顺序修复。不要只确认第一次构建成功,因为第二次构建是否复用缓存才是关键验证。
31.13 增量构建、Build Cache 与 clean
如果只有 clean build 能通过,重点调查:
- 任务输入是否声明完整。
- 代码生成输出是否写入了源码目录。
- 不同任务是否共享或覆盖同一输出目录。
- 外部工具版本、环境变量和系统编码是否作为输入处理。
- 自定义任务是否使用当前时间、随机数或绝对路径。
- 远程 Build Cache 是否返回了不适用于当前环境的输出。
对比运行:
.\gradlew.bat build.\gradlew.bat clean build.\gradlew.bat clean build --no-build-cache.\gradlew.bat clean build --rerun-tasks结果解释:
| 现象 | 优先怀疑 |
|---|---|
| 只在不 clean 时失败 | 增量输入输出、旧生成文件或输出覆盖 |
| 只在启用缓存时失败 | 缓存键、隐式输入或缓存隔离 |
只在 --rerun-tasks 时失败 | 任务本身失败,之前可能一直被跳过 |
| 本地正常、全新目录失败 | 未声明输入、依赖本地文件或环境隐式依赖 |
清理输出可以帮助确认现象,但不能替代修复任务模型。
31.14 构建变慢的分层诊断
先确认变慢发生在哪个阶段:
.\gradlew.bat build --profile.\gradlew.bat build --scan.\gradlew.bat build --info可以按以下顺序定位:
- 启动和 Wrapper 分发包下载。
- Settings、
buildSrc、Included Build 和插件配置。 - 依赖解析和 Artifact Transform。
- Java/Kotlin/Android 编译。
- 测试和测试设备启动。
- Lint、Checkstyle、JaCoCo 等质量任务。
- R8、资源处理和归档。
- CI 排队、依赖下载、缓存恢复和制品上传。
优化前后记录配置时间、执行时间、缓存命中率、下载耗时和失败率。不要一次修改并行、堆内存、缓存和任务图,否则无法知道哪项改动带来了收益或回归。
31.15 Daemon、内存与并行问题
查看 Daemon 状态:
.\gradlew.bat --status.\gradlew.bat --stopDaemon 反复消失或构建出现 OOM 时,检查:
org.gradle.jvmargs是否与 Runner 或开发机内存匹配。- 是否同时运行了多个 Gradle Daemon、IDE 和测试设备。
- Kotlin 编译器、R8、测试 JVM 是否各自占用大量内存。
- CI 容器是否设置了比宿主机更小的内存限制。
- 并行任务是否造成内存峰值,而不是只看平均使用量。
可以临时降低并行度进行对照:
.\gradlew.bat build --no-parallel不要把 --no-daemon 作为所有本地构建的默认修复。CI 是否使用 Daemon 应按 Runner 生命周期、缓存策略和组织规范决定。
31.16 Java、Kotlin 与插件兼容性
兼容性问题通常来自多个版本轴同时变化:
Gradle ↔ Gradle JVMGradle ↔ Kotlin DSL / Groovy插件 ↔ Gradle APIKotlin Plugin ↔ Kotlin CompilerAGP ↔ Gradle / JDK / Android SDK依赖 ↔ 编译字节码 / 运行时字节码收集实际版本:
.\gradlew.bat --version.\gradlew.bat buildEnvironment.\gradlew.bat :build-logic:dependencies升级时采用单变量策略:
- 先固定当前环境并保存成功基线。
- 只升级一个基础版本或插件。
- 执行
help、编译、测试和质量任务。 - 处理弃用和兼容性警告。
- 再进入下一个升级项。
“所有版本都升级到最新”不是兼容性策略。应使用对应版本的官方兼容矩阵和迁移指南。
31.17 CI 与本地行为不一致
本地成功、CI 失败时,逐项对比:
- OS、文件系统大小写和路径分隔符。
- JDK、Gradle Wrapper、Toolchain 和 Android SDK。
GRADLE_USER_HOME、缓存状态和初始化脚本。- 代理、证书、仓库顺序和依赖凭据。
- 环境变量、时区、系统编码和换行符。
- 并行度、容器内存、CPU 数量和磁盘空间。
- Pull Request 是否获得与主分支相同的 Secret。
建议在 CI 保存以下信息:
./gradlew --versionjava -versiongit rev-parse HEAD构建脚本不应依赖开发者本机的 local.properties、全局 Gradle 安装、IDE 环境变量或未提交的用户级插件。
31.18 Android 变体与构建问题
Android 项目常见问题应先确认实际变体和任务:
.\gradlew.bat :app:tasks --all.\gradlew.bat :app:assembleFreeDebug --info.\gradlew.bat :app:dependencies --configuration debugRuntimeClasspath重点检查:
compileSdk、minSdk、AGP、Gradle 和 JDK 兼容性。- Flavor Dimension、Build Type 和
matchingFallbacks。 - Manifest 合并报告和资源合并优先级。
- R8 Mapping、Keep 规则和 Release 与 Debug 的差异。
- SDK Platform、Build Tools、模拟器和设备状态。
- App 与 Library 的变体属性和依赖匹配。
不要用 files(...)、复制 AAR 或关闭 R8 来绕过 Android 变体和发布模型问题。
31.19 依赖与供应链错误
依赖锁定和验证失败时,先区分三类问题:
| 类型 | 典型现象 | 优先动作 |
|---|---|---|
| 解析漂移 | 锁文件出现未预期变化 | 检查动态版本、仓库元数据和更新命令 |
| 内容变化 | 校验和或 PGP 验证失败 | 保存哈希,确认仓库和发布来源,不要立即接受 |
| 来源问题 | 只能从未授权仓库解析 | 检查 Settings 仓库、内容过滤和代理配置 |
常用命令:
.\gradlew.bat dependencies --write-locks.\gradlew.bat --write-verification-metadata sha256 build.\gradlew.bat --dependency-verification strict clean check不要通过删除 gradle/dependency-locks、gradle/verification-metadata.xml 或切换不可信镜像来恢复构建。供应链错误需要保留证据并走依赖升级或事件响应流程。
31.20 把问题缩小为最小复现
最小复现的目标是保留触发错误所需的最少输入:
- 保留同一个 Wrapper 和相关插件版本。
- 删除与问题无关的模块、任务和依赖。
- 固定仓库、JDK、Toolchain 和命令行参数。
- 使用临时目录或全新的
GRADLE_USER_HOME验证。 - 用
--stacktrace、--info或--scan保存完整证据。 - 每次只恢复一个变量,确认哪项输入重新触发问题。
一个可复现问题报告至少应包含:
最小项目或可访问的复现仓库:Gradle Wrapper 版本:JDK 与操作系统:完整执行命令:完整错误输出:预期结果:实际结果:首次出现的版本或 Commit:已尝试的方案及结果:是否涉及缓存、代理或私服:不要把完整仓库、构建扫描或日志直接公开到不受控位置。先清理凭据、内部坐标、路径和业务代码。
31.21 问题分类决策树
可以使用下面的顺序快速缩小范围:
Wrapper/Gradle 能启动吗? 否 -> Wrapper、JAVA_HOME、JDK、网络、权限 是 ↓Settings 和 help 能执行吗? 否 -> pluginManagement、初始化脚本、Settings、构建逻辑 是 ↓依赖能解析吗? 否 -> 仓库、凭据、坐标、锁定、验证、网络 是 ↓任务存在且任务图正确吗? 否 -> 项目路径、插件应用、任务注册、dependsOn 是 ↓变体和 Configuration 能匹配吗? 否 -> 属性、Flavor、Build Type、Usage、Library Elements 是 ↓任务输入输出和缓存正确吗? 否 -> 增量、Build Cache、生成目录、隐式输入 是 ↓外部工具或运行环境失败吗? 是 -> 编译器、测试、R8、SDK、设备、网络、CI 容器31.22 常用排查命令速查
# 环境与项目.\gradlew.bat --version.\gradlew.bat projects.\gradlew.bat properties.\gradlew.bat tasks --all.\gradlew.bat buildEnvironment
# 依赖与变体.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencyInsight --dependency module-name --configuration runtimeClasspath.\gradlew.bat outgoingVariants
# 日志与诊断.\gradlew.bat build --stacktrace.\gradlew.bat build --info.\gradlew.bat build --scan.\gradlew.bat build --profile.\gradlew.bat build --dry-run
# 缓存与重现.\gradlew.bat build --rerun-tasks.\gradlew.bat build --no-build-cache.\gradlew.bat build --no-configuration-cache.\gradlew.bat build --refresh-dependencies.\gradlew.bat --stop命令应根据问题选择,不要把所有参数永久写进 gradle.properties。诊断结束后撤销临时参数,避免后续开发者在不知情的情况下绕过缓存、验证或增量机制。
31.23 修复验证与回归记录
一个修复完成后,至少执行三类验证:
针对性验证:
.\gradlew.bat :affected:relevantTask --stacktrace完整验证:
.\gradlew.bat clean check.\gradlew.bat build环境和重复性验证:
.\gradlew.bat check --no-build-cache.\gradlew.bat check --configuration-cache如果问题与缓存、变体或 CI 有关,还应在对应环境复测。记录修复前后的任务输出、锁文件、验证元数据、构件哈希和构建耗时,避免“这次成功”成为唯一结论。
31.24 常见错误与根因对照表
| 错误表现 | 不推荐的表面修复 | 更可靠的根因方向 |
|---|---|---|
| 依赖无法解析 | 随意增加仓库 | 坐标、仓库、凭据、过滤和验证 |
| 插件找不到 | 把插件当普通依赖添加 | pluginManagement、Plugin Marker 和版本 |
| 字节码版本错误 | 强行降低全部 Java 配置 | 实际运行 JVM、Toolchain 和依赖字节码 |
| 变体不匹配 | 改成 files(...) | 属性、Usage、Flavor、Build Type 和组件模型 |
| 构建只在 clean 后成功 | 每次都执行 clean | 输入输出、生成目录和缓存键 |
| 缓存命中后失败 | 永久关闭缓存 | 修复隐式输入和缓存隔离 |
| R8 后崩溃 | 全局 -keep | 定位反射、JNI、序列化和动态入口 |
| CI 失败本地成功 | 重跑或忽略 CI | JDK、OS、缓存、Secret、代理和权限差异 |
32. 升级策略与兼容性检查
Gradle 升级不是简单地把 gradle-wrapper.properties 中的版本号改大。一次完整升级可能同时影响:
- Gradle Wrapper、Gradle API 和默认行为。
- Gradle JVM、Java Toolchain、Kotlin DSL 和 Groovy。
- Android Gradle Plugin、Kotlin Plugin、KSP、Compose 或其他生态插件。
- 依赖解析、变体匹配、锁文件、验证元数据和仓库行为。
- Configuration Cache、Build Cache、并行执行和构建耗时。
- JAR、AAR、APK、AAB、POM、Module Metadata 和签名构件。
- CI 镜像、开发者 IDE、发布凭据和生产交付流程。
升级的目标不是“使用最新版本”本身,而是在明确兼容边界的前提下获得安全修复、性能改进、功能能力和长期可维护性,同时保留清晰的回滚路径。
32.1 先定义升级目标与范围
升级前先写清楚为什么升级:
| 目标 | 需要重点验证 |
|---|---|
| 修复安全问题 | 受影响组件、依赖来源、锁文件和发布构件 |
| 支持新 JDK | Gradle 运行 JVM、Toolchain、编译和测试 |
| 支持新 Android SDK | AGP、Gradle、Android Studio、SDK 和变体 |
| 消除弃用警告 | --warning-mode all、插件 API 和构建逻辑 |
| 改善构建性能 | 配置时间、任务执行、缓存命中和 CI 成本 |
| 使用新插件能力 | DSL、插件 API、变体、发布和兼容性 |
| 迁移构建脚本 | Groovy/Kotlin DSL、Convention Plugin、目录结构 |
升级范围应明确到具体边界,例如:
Gradle Wrapper:8.14.x → 9.0.x运行 JDK:17 → 21Kotlin Plugin:2.x → 2.yAndroid Gradle Plugin:当前稳定线 → 目标稳定线不要在同一个升级 PR 中同时迁移 Wrapper、JDK、AGP、Kotlin、所有依赖和构建逻辑,除非它们存在强制耦合。变化越多,失败归因和回滚越困难。
32.2 升级前收集基线信息
在升级分支创建前,保存一次可成功构建的基线:
.\gradlew.bat --version.\gradlew.bat projects.\gradlew.bat buildEnvironment.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat help --warning-mode all.\gradlew.bat clean check --scan记录:
- Git Commit、分支和构建时间。
- Gradle Wrapper 版本和分发类型。
- Gradle JVM、Java Toolchain 和 Kotlin/JVM 目标。
- AGP、Kotlin、KSP、Compose、Spring Boot 等插件版本。
- 操作系统、CPU 架构、Android SDK 和 CI 镜像。
- 主要依赖、平台、BOM、锁文件和验证元数据。
- 编译、测试、Lint、R8、打包和发布任务耗时。
- 主要构件的大小、文件列表、SHA-256 和元数据。
- 现有弃用警告、失败测试和已知兼容性例外。
基线的价值在于让升级前后的差异可比较。没有基线,就无法判断升级带来的性能和行为变化。
32.3 建立版本兼容矩阵
至少建立以下矩阵:
| 组件 | 当前版本 | 目标版本 | 兼容来源 | 验证状态 |
|---|---|---|---|---|
| Gradle | Gradle Compatibility Matrix | |||
| Gradle JVM | Gradle Compatibility Matrix | |||
| Java Toolchain | JDK/插件文档 | |||
| Kotlin Plugin | Kotlin/Gradle 文档 | |||
| Android Gradle Plugin | Android 官方兼容表 | |||
| Android Studio | Android 官方发布说明 | |||
| KSP/Compose | 对应插件文档 | |||
| Spring Boot 或其他框架插件 | 插件发布说明 | |||
| 内部 Convention Plugin | 内部测试矩阵 |
重点检查:
- Gradle 与运行 JVM,而不是只检查 Java 编译目标。
- Gradle 内嵌 Kotlin 与 Kotlin DSL 脚本语法。
- 插件是否使用已删除的 Gradle API 或内部类型。
- Android Gradle Plugin 与 Gradle、JDK、Android Studio 和 SDK。
- Kotlin Plugin、KSP、Compose Compiler 和 Kotlin 语言版本。
- 目标平台、操作系统、文件系统和 CPU 架构。
兼容矩阵中的“可以运行”不等于“已经验证”。对生产项目,还应通过自己的完整构建、测试和发布流程确认。
32.4 版本选择与升级顺序
推荐按风险和依赖关系安排顺序:
- 收集基线和官方兼容矩阵。
- 清理已经存在的弃用警告。
- 升级 Wrapper 到目标 Gradle 版本。
- 处理 Gradle API、DSL 和插件兼容问题。
- 升级运行 JDK 或 Java Toolchain。
- 升级 AGP、Kotlin 或其他强耦合插件。
- 升级内部 Convention Plugin 和构建逻辑。
- 单独处理第三方依赖、平台和锁文件。
- 验证缓存、性能、构件和发布流程。
- 在 CI 灰度运行后合并和推广。
不一定所有项目都严格遵循这个顺序。关键是每一步都保持可编译、可测试、可回滚,并在提交说明中记录为什么要改变顺序。
32.5 升级 Gradle Wrapper
推荐通过 Wrapper 任务升级,而不是手工下载 Gradle 并提交未知文件:
.\gradlew.bat wrapper --gradle-version 9.6.1如果需要包含源码和文档的完整分发包:
.\gradlew.bat wrapper ` --gradle-version 9.6.1 ` --distribution-type all升级后检查:
Get-Content gradle\wrapper\gradle-wrapper.properties.\gradlew.bat --version需要注意:一次执行可能主要更新 gradle-wrapper.properties。为了让 Wrapper 脚本和 gradle-wrapper.jar 也完全更新,可以按官方流程再次执行 Wrapper 任务,并审查所有变更。
提交前检查:
git diff -- gradlew gradlew.bat gradle\wrapperWrapper 文件应与项目一起提交。CI、IDE 和开发者都应通过它启动构建,避免“本机全局 Gradle 版本”成为隐式输入。
32.6 处理弃用警告
升级前先把警告完整输出:
.\gradlew.bat help --warning-mode all按来源分类:
- 项目自己的
build.gradle(.kts)。 buildSrc、Convention Plugin 和 Included Build。- 第三方插件。
- Gradle DSL、任务 API 或内部 API。
- Android、Kotlin 或其他生态插件。
处理顺序:
- 优先修复项目自己能控制的警告。
- 查找警告中给出的替代 API 和升级说明。
- 为第三方插件建立版本升级或替换计划。
- 对无法立即升级的插件记录版本上限和风险。
- 在 CI 中保持警告可见,不要长期使用
--warning-mode none。
弃用警告是未来升级风险的提前信号。升级前忽略它们,通常会把小范围迁移变成大版本升级时的集中故障。
32.7 Gradle 大版本迁移
大版本升级应特别关注:
- 已经从弃用状态变为删除的 API。
- 默认值、任务依赖和配置行为变化。
- Configuration Cache、Build Cache 和并行执行行为。
- Kotlin DSL 编译、类型安全访问器和插件解析。
- 文件系统、路径、归档和校验相关行为。
- Java、Groovy、Kotlin 和 Android 集成的支持范围。
建议先在升级分支运行最小任务:
.\gradlew.bat help.\gradlew.bat tasks.\gradlew.bat test.\gradlew.bat check每个阶段只解决当前版本阻塞的问题,不要顺便重构全部构建脚本。功能重构和版本升级分离,才能让失败定位和 Code Review 更清晰。
32.8 插件与构建逻辑升级
插件升级的风险通常高于普通库依赖,因为插件会参与配置阶段、注册任务、修改变体、解析依赖或影响发布。
检查插件:
.\gradlew.bat buildEnvironment.\gradlew.bat :build-logic:dependencies重点审查:
- 插件 ID 和版本是否来自统一的 Version Catalog 或插件管理。
- 插件是否使用 Gradle 内部 API、AGP 内部 API 或未公开类型。
- 插件是否兼容 Configuration Cache、Build Cache 和并行执行。
- 插件注册的任务、扩展和 Configuration 名称是否变化。
- 插件是否改变了依赖仓库、签名、发布或 Secret 读取方式。
- Convention Plugin 是否需要与目标 Gradle 同步升级。
升级内部插件时,优先在 build-logic 自身增加功能测试,再批量迁移所有消费项目。
32.9 Kotlin DSL、Groovy DSL 与脚本编译
Kotlin DSL 的优势是类型安全访问器和 IDE 支持,但升级 Gradle 或插件后,类型安全访问器可能因插件应用顺序、名称变化或模型变化而重新生成。
典型问题包括:
libs、tasks或插件扩展访问器不存在。- 插件应用前访问了插件提供的类型安全模型。
- Kotlin DSL 编译器发现旧 API 已删除或签名变化。
buildSrc或build-logic使用了与 Gradle 内嵌 Kotlin 不兼容的代码。- Groovy 动态属性在 Kotlin DSL 中没有对应类型。
排查时先运行:
.\gradlew.bat help --stacktrace.\gradlew.bat :build-logic:compileKotlin --stacktrace迁移建议:
- 优先使用公开 DSL 和 API,不要依赖内部实现类。
- 将复杂逻辑迁移到类型化插件和测试代码。
- 不要在升级 PR 中同时进行大规模 DSL 风格重写。
- 对公共 Convention Plugin 保持稳定扩展模型,并记录迁移方式。
32.10 JDK、Java Toolchain 与字节码
运行 Gradle 的 JDK 与编译项目的 Toolchain 可以不同,但都必须在兼容矩阵中验证:
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}升级 JDK 时分别验证:
- Gradle 是否支持用目标 JDK 运行。
- Java/Kotlin 编译任务能否使用目标 Toolchain。
- 测试框架和测试 JVM 能否运行。
- Annotation Processor、编译器插件和代码生成工具是否兼容。
- 发布的字节码是否满足消费者和运行环境要求。
- CI 镜像、本地 IDE 和 Android Studio 是否使用同一套约束。
执行:
java -version.\gradlew.bat --version.\gradlew.bat compileJava test不要只修改 sourceCompatibility 或 targetCompatibility。它们主要描述编译目标,不能自动保证运行 Gradle、运行测试和运行插件的 JVM 兼容。
32.11 依赖、Version Catalog 与平台升级
集中管理依赖版本可以降低升级范围:
[versions]jackson = "2.19.2"junit = "5.13.3"
[libraries]jackson-bom = { module = "com.fasterxml.jackson:jackson-bom", version.ref = "jackson" }junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }升级依赖时区分:
- Version Catalog 中的请求版本。
- Platform/BOM 中的对齐版本。
- Dependency Constraint 中的最低或严格版本。
- Lock 文件中的最终解析版本。
- Verification Metadata 中的文件校验和或签名。
Version Catalog 能集中声明,但不会自动强制最终选择的版本。升级后必须重新检查依赖图:
.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencyInsight --dependency jackson --configuration runtimeClasspath不要把“更新了目录中的版本”视为升级完成。还要检查锁文件、验证元数据、漏洞报告、许可证、公共 API 和构件差异。
32.12 锁文件与验证元数据升级
依赖升级应保持声明和解析结果同步:
.\gradlew.bat dependencies --update-locks org.example:example-core.\gradlew.bat --write-verification-metadata sha256 build审查内容:
- 锁文件是否只包含预期模块和 Configuration。
- 是否因为某个平台升级而引入大量传递依赖变化。
- Verification Metadata 中的哈希和 PGP Key 是否来自可信来源。
- 是否出现动态版本、变化模块或未授权仓库。
- SBOM、漏洞扫描和许可证报告是否同步更新。
Release 升级必须在干净环境中验证:
.\gradlew.bat --dependency-verification strict clean check不要在升级 PR 中删除整个锁文件或验证元数据再重新生成。这样会让审查者无法区分预期升级与来源漂移。
32.13 Android、Kotlin 和生态插件升级
Android 项目需要同时处理 AGP、Gradle、JDK、Android Studio、SDK、Kotlin、KSP 和 Compose 等版本轴。
推荐顺序:
- 阅读 AGP Release Notes 和兼容矩阵。
- 确认目标 AGP 所需 Gradle 和 JDK。
- 升级 Wrapper 并处理 Gradle 弃用。
- 升级 AGP,修复 DSL 和 Variant API 变化。
- 升级 Kotlin、KSP、Compose 或相关编译插件。
- 验证每个关键 Flavor、Build Type、APK/AAB 和设备测试。
- 检查 Manifest、资源、R8、签名和发布渠道。
Android 验证命令:
.\gradlew.bat :app:assembleDebug.\gradlew.bat :app:testDebugUnitTest.\gradlew.bat :app:lintDebug.\gradlew.bat :app:assembleRelease.\gradlew.bat :app:bundleRelease.\gradlew.bat :app:signingReport不要在 AGP 升级时同时重构所有 Source Set、Flavor 和发布流程。先建立旧版本与新版本的同变体对照,再逐步迁移。
32.14 API、行为与构件差异验证
升级通过编译和测试,不代表输出完全兼容。应比较:
- JAR、AAR、APK、AAB 文件列表和大小。
- Manifest、资源、BuildConfig 和生成代码。
- POM、Gradle Module Metadata 和传递依赖。
- 公共 API、二进制兼容性和 Java 字节码版本。
- R8 Mapping、Keep 规则和混淆结果。
- 签名指纹、校验和和 SBOM。
可以保存升级前后的依赖树:
.\gradlew.bat dependencies --configuration runtimeClasspath > before-dependencies.txt.\gradlew.bat dependencies --configuration runtimeClasspath > after-dependencies.txt对关键库使用独立 Consumer 验证,而不是只在生产者项目自身的类路径中测试。生产者的测试依赖可能掩盖漏发、错误作用域或错误变体。
32.15 测试策略与升级门禁
升级验证至少分三层:
快速门禁:
.\gradlew.bat help tasks compileJava test完整验证:
.\gradlew.bat clean check build交付验证:
.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository.\gradlew.bat distZipAndroid 或集成项目还应加入:
- 关键 Build Variant 编译。
- JVM 单元测试和 Instrumentation 测试。
- Lint、R8、资源收缩和签名验证。
- 独立 Consumer、容器或部署环境测试。
- 生成 SBOM、校验和、报告和构件清单。
升级 PR 不应只执行 help。最小验证应覆盖升级真正影响的任务路径。
32.16 缓存与性能回归
升级可能改变配置时间、任务输入、缓存键、插件行为和任务图。对比以下指标:
- 配置阶段耗时。
- 关键任务执行耗时。
FROM-CACHE、UP-TO-DATE和实际执行数量。- 依赖下载和 Artifact Transform 耗时。
- CI 总耗时、排队时间和缓存恢复时间。
- 内存峰值、Daemon 数量和 Runner 成本。
对照执行:
.\gradlew.bat clean check --no-build-cache.\gradlew.bat clean check --build-cache.\gradlew.bat check --configuration-cache.\gradlew.bat check --scan如果升级后只有缓存构建失败,优先检查任务输入输出和插件兼容性;不要永久关闭缓存掩盖问题。如果配置缓存报告出现问题,应处理构建逻辑,而不是长期使用宽松忽略模式。
32.17 CI 灰度与分阶段推广
升级应先在独立 CI 流程灰度:
升级分支 -> Linux + 主 JDK 快速验证 -> 多 JDK/多平台矩阵 -> Android/集成测试 -> Staging 发布和 Consumer 验证 -> 主分支合并队列 -> 受保护 Release 流程灰度期间保留:
- 升级分支和原始基线分支。
- 同一 Commit 的前后构建日志。
- 失败任务、报告、Build Scan 和构件哈希。
- 目标版本的兼容矩阵和核对日期。
- 允许暂时失败的实验任务及其失效期限。
不要让升级分支直接写入正式发布仓库、共享远程 Build Cache 或生产环境 Secret。升级验证应与正式发布权限隔离。
32.18 回滚准备与版本冻结
升级提交应保持小而可逆。保留:
- 旧 Wrapper 配置和 Wrapper 校验记录。
- 升级前的锁文件、验证元数据和 Version Catalog。
- 升级前后的依赖树、构件哈希和测试报告。
- 旧版本的 CI 镜像、JDK 和 Android SDK 信息。
- 失败任务、已知行为变化和临时兼容补丁。
- 可以重新生成或重新获取的测试构件,而不是覆盖正式版本。
如果升级影响发布流程,先冻结新的正式版本发布,完成 Staging 和 Consumer 验证后再恢复。不要通过覆盖相同 Release 坐标来回滚,因为这会破坏缓存和消费方的可复现性。
32.19 升级提交与变更记录
一个可审查的升级 PR 应包含:
升级目标:当前版本 → 目标版本:兼容矩阵来源:升级顺序与原因:处理的弃用和迁移:锁文件变化:验证元数据变化:依赖图变化:构件大小/哈希变化:测试和质量结果:性能结果:已知风险:回滚方式:提交拆分建议:
- 先处理弃用和准备性重构。
- 单独升级 Wrapper。
- 单独升级核心插件或 AGP。
- 单独升级 JDK/Toolchain。
- 单独更新依赖、锁文件和验证元数据。
- 最后更新文档、CI 矩阵和默认版本。
如果多个版本必须一起变化,也应在提交信息中说明耦合原因和验证范围。
32.20 升级自动化与定期检查
可以使用定时 CI 或依赖更新工具发现升级机会,但自动生成 PR 不等于自动合并:
- Wrapper 升级需要验证 Gradle、JDK 和插件矩阵。
- 插件升级需要验证构建逻辑和 Configuration Cache。
- 依赖升级需要更新锁文件、验证元数据、SBOM 和漏洞状态。
- Android 升级需要验证关键变体、设备测试和 Release 构件。
- 大版本升级需要检查迁移指南和弃用移除。
定期任务可以执行:
.\gradlew.bat help --warning-mode all.\gradlew.bat dependencyUpdates.\gradlew.bat clean check --scandependencyUpdates 是否存在取决于项目是否应用相应的第三方插件,不能把它当作 Gradle 内置任务。自动升级任务应产生可审查的 Pull Request,不应直接修改主分支或正式仓库。
32.21 升级完成定义
一次升级只有在以下条件全部满足时才算完成:
版本与兼容性:
- Wrapper、Gradle、JDK、Toolchain 和核心插件处于兼容矩阵中。
- 目标平台、Android SDK、Kotlin、AGP 或框架插件已验证。
- 公开 API、内部构建逻辑和插件没有未处理的弃用警告。
构建与依赖:
- 编译、测试、静态分析和集成测试通过。
- 锁文件、Version Catalog、平台和验证元数据已审查。
- 依赖图、漏洞、许可证和 SBOM 变化有记录。
- 关键变体、Configuration 和发布组件均能消费。
性能与交付:
- 配置、执行、缓存和 CI 耗时没有不可接受的回归。
- Build Cache 和 Configuration Cache 已验证或明确记录不兼容项。
- JAR、AAR、APK、AAB、POM、Module Metadata 和签名构件符合预期。
- Staging Consumer 和发布模拟验证通过。
- 回滚步骤、旧版本基线和升级文档已保留。
33. 完整最小项目
前面的章节分别介绍了 Gradle 的模型、任务、依赖、测试、发布和 CI。为了把这些概念串起来,本章从零开始构建一个可以运行、测试、打包和持续集成的 Java Application。
这个示例刻意保持“小而完整”:
- 使用 Kotlin DSL。
- 使用 Gradle Wrapper。
- 使用 Java Toolchain。
- 使用 Version Catalog 管理测试依赖。
- 使用
application插件运行和打包应用。 - 使用 JUnit Jupiter 编写测试。
- 增加一个简单的自定义验证任务。
- 展示从单项目演进到多项目和发布的路径。
- 提供本地验证、CI 验证和排错命令。
示例项目名称为 hello-gradle,包名为 com.example。代码可以作为学习模板,但正式项目仍应替换组织坐标、版本、许可证、仓库和 CI Secret。
33.1 创建项目的两种方式
可以手工创建目录,也可以使用 Gradle Build Init Plugin:
gradle init ` --type java-application ` --dsl kotlin ` --test-framework junit-jupiter ` --package com.example ` --project-name hello-gradle ` --java-version 21 ` --no-split-project如果当前机器没有全局 Gradle,可以先在临时目录使用已安装的 Gradle 创建 Wrapper,再把生成的 Wrapper 和文件提交到项目。更常见的方式是从已有模板或内部脚手架开始。
初始化后优先检查:
.\gradlew.bat --version.\gradlew.bat tasks不要直接接受生成文件中的每一项配置。先理解插件、仓库、版本目录、测试框架和 Wrapper,再按项目约束调整。
33.2 最终单项目目录
完整的单项目结构如下:
hello-gradle/├── .gitignore├── gradlew├── gradlew.bat├── gradle/│ ├── libs.versions.toml│ └── wrapper/│ ├── gradle-wrapper.jar│ └── gradle-wrapper.properties├── gradle.properties├── settings.gradle.kts├── build.gradle.kts└── src/ ├── main/ │ └── java/ │ └── com/example/ │ ├── App.java │ └── GreetingService.java └── test/ └── java/ └── com/example/ ├── AppTest.java └── GreetingServiceTest.java目录职责:
settings.gradle.kts:定义构建边界、项目名、仓库和项目包含关系。build.gradle.kts:应用插件、声明 Java、依赖、应用入口和任务配置。gradle/libs.versions.toml:集中管理依赖别名和版本。gradle.properties:放置非敏感的构建属性。src/main:生产源码和资源。src/test:本地 JVM 测试。build:所有生成输出,不提交到版本控制。
33.3 settings.gradle.kts
import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement { repositories { gradlePluginPortal() mavenCentral() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() }}
rootProject.name = "hello-gradle"这个文件完成三件事:
- 配置插件解析仓库。
- 配置项目依赖仓库,并禁止子项目私自添加仓库。
- 为当前 Build 设置根项目名称。
单项目也应该保留 settings.gradle.kts,因为后续增加子项目、Included Build、Version Catalog 或集中仓库时都需要它。
33.4 gradle.properties
建议只放置非敏感、可提交的构建属性:
org.gradle.caching=trueorg.gradle.parallel=trueorg.gradle.configuration-cache=trueorg.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8需要注意:
- Configuration Cache 是否启用,要以插件兼容性和实际收益为准。
- CI 可以通过命令行或环境变量覆盖部分配置。
- 用户名、密码、Token、签名口令和私钥不得放入提交的
gradle.properties。 - 团队约定和用户本地设置可以放在用户级
~/.gradle/gradle.properties。 - 如果构建在低内存 CI Runner 上运行,
-Xmx2g需要按容器内存调整。
33.5 gradle/libs.versions.toml
使用 Version Catalog 管理测试框架版本:
[versions]junit = "6.0.3"
[libraries]junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }Version Catalog 的价值在于:
- 依赖别名统一,减少字符串重复。
- 多个模块可以共享版本意图。
- 依赖升级变更集中且易于审查。
- 可以继续配合依赖锁定、约束和验证元数据。
目录不会自动固定传递依赖,也不会自动完成漏洞扫描。生产项目仍应维护锁文件、校验和和升级记录。
33.6 build.gradle.kts
完整的应用构建脚本:
import org.gradle.api.tasks.testing.Testimport org.gradle.jvm.toolchain.JavaLanguageVersion
plugins { application}
group = "com.example"version = "1.0.0"
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
dependencies { testImplementation(libs.junit.jupiter)}
application { mainClass = "com.example.App"}
tasks.withType<Test>().configureEach { useJUnitPlatform()}插件提供能力:
application隐式应用 Java 和 Distribution 能力。java {}配置编译和运行使用的 Toolchain。dependencies {}声明测试依赖。application {}声明入口类。tasks.withType<Test>()统一配置测试任务。
33.7 .gitignore
.gradle/build/!gradle/wrapper/gradle-wrapper.jar!gradle/wrapper/gradle-wrapper.properties../.idea/*.imlout/.DS_Store不要忽略以下关键文件:
gradlew和gradlew.bat。gradle/wrapper/gradle-wrapper.jar。gradle/wrapper/gradle-wrapper.properties。settings.gradle.kts和build.gradle.kts。gradle/libs.versions.toml。gradle/dependency-locks和gradle/verification-metadata.xml,如果项目启用了对应机制。
33.8 src/main/java/com/example/GreetingService.java
把可测试的业务逻辑从入口类中分离:
package com.example;
public final class GreetingService { public String greeting(String name) { if (name == null || name.isBlank()) { return "Hello, Gradle"; } return "Hello, " + name.trim(); }}这样做的好处:
main只负责组装输入和输出。- 业务逻辑可以通过普通单元测试验证。
- 后续替换命令行、HTTP 或其他入口时不需要复制逻辑。
- 测试失败时更容易定位是应用入口还是业务类的问题。
33.9 src/main/java/com/example/App.java
package com.example;
public final class App { private App() { }
public static void main(String[] args) { String name = args.length == 0 ? "Gradle" : args[0]; GreetingService service = new GreetingService(); System.out.println(service.greeting(name)); }}运行时可以传入命令行参数:
.\gradlew.bat run --args="Alice"预期输出:
Hello, Alice不传参数时:
.\gradlew.bat run预期输出:
Hello, Gradle33.10 src/test/java/com/example/GreetingServiceTest.java
package com.example;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class GreetingServiceTest { private final GreetingService service = new GreetingService();
@Test void greetsNamedUser() { assertEquals("Hello, Alice", service.greeting("Alice")); }
@Test void usesDefaultNameForBlankInput() { assertEquals("Hello, Gradle", service.greeting(" ")); }}33.11 src/test/java/com/example/AppTest.java
package com.example;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.lang.reflect.Method;import org.junit.jupiter.api.Test;
class AppTest { @Test void applicationHasMainMethod() throws NoSuchMethodException { Method main = App.class.getMethod("main", String[].class); assertNotNull(main); }}上面的反射断言只是演示测试类结构。真实项目应优先测试可观察行为,而不是过度测试实现细节。更直接的入口测试可以通过 ApplicationPlugin 的运行任务、TestKit 或端到端测试完成。
33.12 测试任务与报告
运行测试:
.\gradlew.bat test常用验证命令:
.\gradlew.bat test --tests "com.example.GreetingServiceTest".\gradlew.bat test --info.\gradlew.bat test --rerun-tasks测试报告通常位于:
build/reports/tests/test/index.htmlbuild/test-results/test/--rerun-tasks 只适合诊断测试缓存或增量问题,不应成为常规命令。测试失败时先看测试报告和实际异常,再决定是否需要重新执行。
33.13 自定义项目验证 Task
给最小项目增加一个目录约定校验:
import org.gradle.api.DefaultTaskimport org.gradle.api.file.DirectoryPropertyimport org.gradle.api.tasks.InputDirectoryimport org.gradle.api.tasks.PathSensitiveimport org.gradle.api.tasks.PathSensitivityimport org.gradle.api.tasks.TaskAction
abstract class VerifyProjectLayout : DefaultTask() { @get:InputDirectory @get:PathSensitive(PathSensitivity.RELATIVE) abstract val sourceDirectory: DirectoryProperty
@TaskAction fun verify() { val appFile = sourceDirectory.file("com/example/App.java").asFile if (!appFile.isFile) { throw GradleException("Missing application entry: $appFile") } }}
tasks.register<VerifyProjectLayout>("verifyProjectLayout") { group = "verification" description = "Checks the minimal project layout." sourceDirectory.set(layout.projectDirectory.dir("src/main/java"))}
tasks.named("check") { dependsOn("verifyProjectLayout")}运行:
.\gradlew.bat verifyProjectLayout.\gradlew.bat check这个 Task 的重点不是校验规则本身,而是展示如何把输入、失败信息和生命周期连接起来。
33.14 完整验证顺序
建议按成本从低到高执行:
.\gradlew.bat --version.\gradlew.bat tasks.\gradlew.bat verifyProjectLayout.\gradlew.bat test.\gradlew.bat run --args="Alice".\gradlew.bat jar.\gradlew.bat installDist.\gradlew.bat distZip.\gradlew.bat build每一步的目标:
--version:确认 Wrapper 和 JVM。tasks:确认插件和任务已加载。verifyProjectLayout:验证自定义任务。test:验证业务逻辑。run:验证应用入口和运行时类路径。jar:验证 JAR 构建。installDist:验证可运行目录分发。distZip:验证压缩分发包。build:执行完整生命周期。
33.15 查看 build/ 输出
执行 build 后,常见输出如下:
build/├── classes/java/main/├── classes/java/test/├── generated/version.properties├── install/hello-gradle/│ ├── bin/hello-gradle│ ├── bin/hello-gradle.bat│ └── lib/hello-gradle-1.0.0.jar├── libs/hello-gradle-1.0.0.jar├── reports/tests/test/├── test-results/test/└── distributions/ ├── hello-gradle-1.0.0.tar └── hello-gradle-1.0.0.zip检查 JAR 内容:
jar tf build\libs\hello-gradle-1.0.0.jar检查 Manifest:
jar xf build\libs\hello-gradle-1.0.0.jar META-INF\MANIFEST.MFGet-Content META-INF\MANIFEST.MFbuild/ 是可删除、可重新生成的输出目录。不要手工修改其中的文件并期待下次构建保留这些修改。
33.16 配置可执行分发
application 插件会生成启动脚本和包含运行时依赖的分发包:
application { mainClass = "com.example.App" applicationDefaultJvmArgs = listOf( "-Dfile.encoding=UTF-8" )}
distributions { main { distributionBaseName = "hello-gradle" contents { from("README.md") from("config") { into("conf") } } }}执行:
.\gradlew.bat installDistbuild\install\hello-gradle\bin\hello-gradle.bat Alice分发包适合命令行应用交付。类库项目不应为了方便而使用 Application Distribution 代替正常 Maven/Ivy 发布。
33.17 添加 README 与运行说明
项目根目录可以添加 README.md:
# hello-gradle
## 环境要求
- JDK 21- Gradle Wrapper
## 验证
````powershell.\gradlew.bat clean check运行
.\gradlew.bat run --args="Alice"文档应说明:
- 使用哪个 Wrapper 和 JDK。- 如何运行测试和应用。- 如何构建分发包。- 依赖、许可证和配置来源。- CI 入口和发布条件。- 常见问题和联系方式。
### 33.18 逐步演进为多项目
当应用出现可复用核心逻辑时,可以拆成 `core` 和 `app` 两个子项目:
```texthello-gradle/├── settings.gradle.kts├── build.gradle.kts├── core/│ ├── build.gradle.kts│ └── src/main/java/com/example/core/│ └── GreetingService.java└── app/ ├── build.gradle.kts └── src/main/java/com/example/app/ └── App.javasettings.gradle.kts:
rootProject.name = "hello-gradle"include(":core", ":app")根 build.gradle.kts:
plugins { `java-library` apply false application apply false}core/build.gradle.kts:
plugins { `java-library`}
dependencies { testImplementation(libs.junit.jupiter)}
tasks.withType<Test>().configureEach { useJUnitPlatform()}app/build.gradle.kts:
plugins { application}
dependencies { implementation(project(":core")) testImplementation(libs.junit.jupiter)}
application { mainClass = "com.example.app.App"}
tasks.withType<Test>().configureEach { useJUnitPlatform()}执行指定模块任务:
.\gradlew.bat :core:test.\gradlew.bat :app:run --args="Alice".\gradlew.bat build拆分模块后,公共逻辑应通过 implementation(project(":core")) 建立项目依赖,不要通过本地 JAR 绕过项目模型。
33.19 为类库模块增加发布演练
如果 core 需要供其他项目消费,可以在 core/build.gradle.kts 中增加本地发布:
plugins { `java-library` `maven-publish`}
java { withSourcesJar() withJavadocJar()}
group = "com.example"version = "1.0.0-SNAPSHOT"
publishing { publications { create<MavenPublication>("mavenJava") { from(components["java"]) pom { name = "Hello Gradle Core" description = "Reusable greeting APIs." } } }
repositories { maven { name = "stagingDirectory" url = layout.buildDirectory.dir("staging-repo") } }}执行:
.\gradlew.bat :core:publishAllPublicationsToStagingDirectoryRepository发布类库时,必须检查 POM、Module Metadata、sources、javadoc、依赖作用域和版本不可变性。应用模块本身通常不作为普通 Maven 类库发布。
33.20 依赖锁定与验证演练
多项目示例可以启用依赖锁定:
configurations.configureEach { resolutionStrategy.activateDependencyLocking()}生成锁文件:
.\gradlew.bat dependencies --write-locks生成依赖验证元数据:
.\gradlew.bat --write-verification-metadata sha256 clean check提交前检查:
git diff -- gradle/dependency-locks gradle/verification-metadata.xml gradle/libs.versions.toml不要在学习项目中盲目复制安全文件内容。先理解它们记录的 Configuration、构件哈希、来源和更新流程,再决定哪些机制适合正式项目。
33.21 GitHub Actions 最小 CI
根目录创建 .github/workflows/gradle.yml:
name: Gradle build
on: push: branches: [main] pull_request:
permissions: contents: read
jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6
- name: Setup Java uses: actions/setup-java@v5 with: distribution: temurin java-version: '21'
- name: Setup Gradle uses: gradle/actions/setup-gradle@v6
- name: Verify run: ./gradlew clean check --no-daemon --stacktrace
- name: Upload reports if: ${{ always() }} uses: actions/upload-artifact@v4 with: name: gradle-reports path: | **/build/reports/** **/build/test-results/** if-no-files-found: ignoreCI 需要拥有一致的 JDK、Wrapper、仓库和 Secret 策略。普通 Pull Request 不应执行远程发布或使用签名密钥。
33.22 常用操作与预期结果
| 操作 | 命令 | 预期结果 |
|---|---|---|
| 查看版本 | gradlew --version | 显示 Wrapper 和 JVM |
| 查看任务 | gradlew tasks | 显示应用、测试和分发任务 |
| 编译测试 | gradlew test | 测试通过并生成报告 |
| 运行应用 | gradlew run --args="Alice" | 输出问候语 |
| 生成 JAR | gradlew jar | build/libs 中出现 JAR |
| 安装分发 | gradlew installDist | build/install 出现启动目录 |
| 生成压缩包 | gradlew distZip | build/distributions 出现 ZIP |
| 完整验证 | gradlew clean check | 编译、测试和自定义校验通过 |
| 查看依赖 | gradlew dependencies | 输出依赖树 |
| 查看构建扫描 | gradlew build --scan | 生成构建诊断信息 |
Windows 下使用 gradlew.bat,Linux 和 macOS 下使用 ./gradlew。文档中的命令应根据操作系统替换 Wrapper 调用方式。
33.23 常见错误与排查
找不到入口类
检查 application.mainClass 是否与源码包名和类名完全一致,并确认类位于 src/main/java:
.\gradlew.bat classes.\gradlew.bat run --stacktrace测试没有被发现
确认测试位于 src/test/java,使用 JUnit Jupiter 注解,并且测试任务配置了:
tasks.withType<Test>().configureEach { useJUnitPlatform()}libs.junit.jupiter 无法访问
检查 gradle/libs.versions.toml 的别名、TOML 语法和文件位置。修改目录后重新运行 tasks 或 help 让类型安全访问器重新生成。
子项目依赖无法解析
确认 settings.gradle.kts 中包含模块,并使用:
implementation(project(":core"))不要把 core/build/libs/*.jar 直接加入 App。
CI 与本地不一致
比较 Wrapper、JDK、仓库、缓存、操作系统、环境变量和工作区是否干净。先执行 gradlew --version 和 gradlew help 建立基线。
33.24 从最小项目继续学习
建议按以下顺序扩展,而不是一次引入所有插件:
- 增加资源文件和
processResources。 - 增加一个第三方生产依赖并观察依赖树。
- 添加 Version Catalog 中的多个别名。
- 启用依赖锁定和验证元数据。
- 把公共代码拆成
core子项目。 - 将共享规则迁移到
build-logicConvention Plugin。 - 为
core增加 sources、javadoc 和本地 Maven 发布。 - 为自定义 Task 增加输入输出和缓存验证。
- 启用 Build Cache 和 Configuration Cache 并观察收益。
- 将测试、构建、制品和发布接入受保护 CI/CD。
每一步只引入一个主要概念,并记录新增文件、任务、依赖和构建输出。
33.25 完整项目检查清单
项目基础:
-
settings.gradle.kts、build.gradle.kts和 Wrapper 已提交。 -
gradle/libs.versions.toml中的版本和别名可解释。 - JDK、Toolchain 和 Gradle 版本明确。
-
.gitignore不会忽略必要的构建文件。 - Secret 没有进入源码、属性文件和构建输出。
代码与测试:
- 主类、包名和
application.mainClass一致。 - 业务逻辑与入口分离,拥有可读的单元测试。
-
test使用正确测试平台并生成报告。 - 自定义 Task 声明输入输出并接入
check。 -
run、jar、installDist和distZip都经过验证。
依赖与发布:
- 依赖仓库集中声明。
- 关键依赖可以通过 Version Catalog、平台或约束治理。
- 类库模块使用项目依赖,不使用本地 JAR 绕过模型。
- 发布前验证 POM、Module Metadata、sources 和 javadoc。
- 锁文件、验证元数据和 SBOM 按项目策略维护。
CI/CD:
- CI 使用 Wrapper 和固定 JDK。
- Pull Request 默认执行
check,不使用发布 Secret。 - 测试报告和构建制品可下载。
- 发布只由受保护 Tag、分支或审批触发。
- 构建、产物、Commit、哈希和报告可以互相追踪。
34. 最佳实践与工程治理
Gradle 最佳实践不是一组必须机械复制的配置,而是一套围绕可维护性、可复现性、性能、安全和团队协作形成的工程约束。好的构建系统应该让“正确做法容易被采用”,让错误配置能够尽早失败,让构建结果可以解释、验证和追踪。
本章把最佳实践整理为五个层次:
基础层:Wrapper、JDK、目录、版本和仓库模型层:Settings、Project、Plugin、Task、Configuration 和变体效率层:懒配置、增量构建、缓存、并行和配置缓存治理层:依赖、供应链、Secret、发布和 CI/CD协作层:文档、评审、迁移、指标和长期维护不要一次性把所有优化开关都打开。先建立基线,再按照风险和收益逐项落地。
34.1 基础一致性
项目应先建立稳定的构建入口和环境边界:
- 总是优先使用 Wrapper,不依赖开发者机器的全局 Gradle。
- 将
gradlew、gradlew.bat、gradle-wrapper.jar和gradle-wrapper.properties一起提交。 - 明确 Gradle、JDK、Kotlin、AGP、插件和 Toolchain 版本。
- 在 CI 与本地使用同一套 Wrapper 和受支持的 JDK 范围。
- 新项目优先使用 Kotlin DSL,旧项目根据迁移收益逐步迁移。
- 不把 IDE 的隐式配置当作构建输入。
- 不把本机路径、用户名、临时目录或 Secret 写入产物。
基础信息应能通过命令快速确认:
.\gradlew.bat --version.\gradlew.bat projects.\gradlew.bat properties34.2 仓库结构与项目边界
单项目和多项目都应使用清晰的 settings.gradle.kts 作为构建边界:
rootProject.name = "example-platform"
include(":app")include(":core")include(":api")include(":feature:search")
pluginManagement { includeBuild("build-logic")}目录职责建议保持稳定:
example-platform/├── settings.gradle.kts├── build.gradle.kts├── gradle.properties├── gradle/│ ├── libs.versions.toml│ └── wrapper/├── build-logic/├── app/├── core/├── api/└── docs/组织项目时遵循:
- 一个模块有清晰、可解释的职责。
- 公共 API、实现细节、应用入口和测试夹具分开建模。
- 测试代码、集成测试、基准测试和构建逻辑使用明确的 Source Set 或 Included Build。
- 不把所有模块都自动应用所有插件。
- 不用目录层级掩盖实际依赖方向。
- 模块拆分以边界和独立变更为依据,而不是只追求模块数量。
34.3 Settings 文件的职责
settings.gradle.kts 负责构建结构和全局解析边界,不应变成第二个巨大业务脚本。适合放置:
rootProject.name和include。pluginManagement。dependencyResolutionManagement。- 仓库集中声明与内容过滤。
- Version Catalog 导入。
- Included Build 和构建逻辑入口。
- 少量全局构建参数和命名规则。
示例:
import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement { repositories { gradlePluginPortal() mavenCentral() google() }}
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { mavenCentral() google() }}避免在 Settings 配置阶段:
- 访问远程 HTTP API。
- 扫描整个工作区并执行复杂解析。
- 读取只有 CI 才存在的 Secret。
- 动态生成大量项目或依赖版本。
- 通过全局变量修改所有 Project 的内部状态。
34.4 脚本设计原则
构建脚本应描述模型和约定,而不是按命令式脚本逐行执行:
- 使用
plugins {}管理插件。 - 使用
tasks.register、named和configureEach。 - 使用 Provider API 表达延迟值。
- 配置阶段不做网络请求、编译、外部进程和大规模 I/O。
- 任务动作只在执行阶段读取最终值并执行工作。
- 将重复逻辑提取为 Convention Plugin。
- 少用跨项目
allprojects和subprojects。 - 不通过
afterEvaluate修补不清晰的插件顺序。 - 不依赖任务名称排序来表达任务关系。
不推荐:
allprojects { tasks.getByName("build") file("config/version.txt").readText() exec { commandLine("git", "rev-parse", "HEAD") }}推荐把这些行为拆成明确的插件、Provider、任务输入和任务依赖,使其只在需要的项目和任务中发生。
34.5 依赖与插件治理
依赖管理要同时考虑版本、作用域、来源、变体和安全性:
- 使用 Version Catalog 管理别名和共享版本。
- 使用 Platform/BOM 对齐同一生态的组件。
- 库模块优先使用
implementation,只有公共 API 暴露时才使用api。 - 对传递依赖使用约束表达最低版本或已知不兼容版本。
- 不使用
latest.release、1.+等动态版本作为 Release 输入。 - 关键项目启用 Dependency Locking 和 Dependency Verification。
- 用
dependencies和dependencyInsight解释每次版本冲突。 - 插件版本也需要固定、审查和纳入供应链治理。
- 仓库集中声明,并对内部仓库使用内容过滤或独占内容。
例如:
dependencies { implementation(libs.jackson.databind) implementation(platform(libs.jackson.bom))
constraints { implementation("org.example:example-core:1.8.2") { because("Approved version for the current release line") } }}版本目录提高一致性,但不会自动固定最终解析结果,也不会替代锁文件、校验和、漏洞扫描或许可证审批。
34.6 任务建模与输入输出
自定义 Task 应完整声明输入输出:
- 使用
@Input、@InputFile、@InputFiles、@InputDirectory和@Classpath。 - 使用
@OutputFile、@OutputDirectory和@LocalState描述结果。 - 为文件输入选择正确的
PathSensitivity。 - 输出放在
build或明确的构建目录。 - 不把源码目录、用户目录或公共临时目录作为可写输出。
- 将工具版本、配置文件、环境参数和生成规则作为输入。
- 只在结果稳定且可复现时启用任务输出缓存。
任务模型完整后,Gradle 才能正确进行:
输入未变化 -> UP-TO-DATE 或 FROM-CACHE输入变化 -> 只执行受影响的任务输出缺失 -> 执行必要的生成任务如果必须执行外部工具、访问设备或读取网络状态,应明确其不可缓存边界,不能让 Gradle 猜测结果。
34.7 插件与构建逻辑复用
多模块项目建议采用分层复用:
build-logic/├── company.base.gradle.kts├── company.java-library.gradle.kts├── company.android-library.gradle.kts├── company.quality.gradle.kts└── company.publishing.gradle.kts选择方式:
- 仅一次使用的简单逻辑:保留在模块脚本中。
- 同一项目多个模块共享的规则:使用
build-logicConvention Plugin。 - 跨仓库共享且需要独立版本的能力:使用二进制插件。
- 复杂任务逻辑:定义类型安全的 Task 类型并进行测试。
- 多个任务需要共享连接或受限资源:使用 Build Service。
- 大量独立工作单元:使用 Worker API。
插件设计应提供默认约定,但允许模块通过扩展属性进行合理覆盖。不要把所有模块都强制变成完全相同的项目。
34.8 构建性能治理
性能优化应遵循“测量—定位—修改—复测”的闭环:
.\gradlew.bat build --profile.\gradlew.bat build --scan.\gradlew.bat build --dry-run优先级通常是:
- 减少不必要的任务和变体。
- 修复配置阶段的网络、I/O 和过早求值。
- 使用任务配置避免和 Provider API。
- 补全自定义任务输入输出,恢复增量执行。
- 评估本地和远程 Build Cache。
- 逐步迁移 Configuration Cache。
- 再考虑并行度、JVM 堆和 CI Runner 规格。
不要在没有基线的情况下直接增加 org.gradle.jvmargs、开启所有并行或堆叠多个缓存。性能改动应记录:
- 配置时间和执行时间。
- 任务执行、跳过和缓存命中数量。
- 依赖下载、缓存恢复和制品上传时间。
- CI 排队时间、Runner 成本和失败率。
- 改动前后的可复现性和报告差异。
34.9 Build Cache 与 Configuration Cache
两种缓存职责不同:
| 机制 | 缓存内容 | 主要问题 |
|---|---|---|
| 增量构建 | 当前工作区的任务状态 | 输入输出是否声明完整 |
| Build Cache | 可复用任务输出 | 输出是否稳定、缓存键是否完整 |
| Configuration Cache | 配置阶段生成的任务图 | 构建逻辑是否兼容配置缓存 |
推荐逐步启用:
org.gradle.caching=trueorg.gradle.configuration-cache=true启用前应先处理:
- 配置阶段读取 Project、Settings 或 Gradle 对象。
- 未声明的环境变量、系统属性和文件输入。
- 不同 OS、JDK、架构之间的缓存隔离。
- 远程缓存的读写权限和不可信分支写入风险。
- 插件、Build Service、Worker API 和自定义任务的兼容性。
缓存的唯一目标是减少可靠构建的时间。如果缓存增加了错误、维护成本或安全风险,应先修复构建模型,再决定是否启用。
34.10 可复现构建
可复现构建要求相同输入尽量生成相同结果。需要控制:
- Wrapper、JDK、Toolchain 和插件版本。
- 依赖版本、锁文件和验证元数据。
- 归档文件顺序、时间戳和 Manifest 内容。
- 生成代码中的时间、随机数和绝对路径。
- 操作系统字符集、换行符和文件系统差异。
- 外部工具版本和命令参数。
- CI 中的环境变量和工作区状态。
可以对关键构件进行哈希比较:
.\gradlew.bat clean buildGet-FileHash build\libs\example-core-1.0.0.jar -Algorithm SHA256
.\gradlew.bat clean buildGet-FileHash build\libs\example-core-1.0.0.jar -Algorithm SHA256如果结果不同,应比较归档条目、Manifest、生成文件和依赖元数据,不要先关闭缓存或跳过验证。
34.11 测试与质量门禁
check 应成为项目统一的验证入口:
tasks.named("check") { dependsOn("verifyBuildConventions")}质量门禁可以包括:
- 编译和单元测试。
- 集成测试、设备测试或端到端测试。
- JaCoCo 覆盖率和覆盖率阈值。
- Checkstyle、Spotless、Detekt、Ktlint 或 Android Lint。
- API 兼容性检查。
- 依赖锁定、校验和、许可证和漏洞扫描。
- SBOM 生成与构件哈希记录。
- 发布前 Consumer 验证。
不同反馈速度的检查应分层:
Pull Request:快速编译、单元测试、静态检查main:完整测试、构建、SBOM、性能和集成验证Release:签名、Staging Consumer、发布和回滚验证不要为了让 CI 更快而完全删除质量门禁。应通过任务分层、缓存、并行和测试选择优化反馈时间。
34.12 CI/CD 与制品管理
CI/CD 应使用构建一次、验证一次、提升同一份构件的模型:
Pull Request -> check -> 构建候选构件 -> 保存测试报告和 SBOM
main -> 完整验证 -> 上传版本化构件 -> Staging Consumer
受保护 Tag -> 签名 -> 发布到正式仓库 -> 记录哈希和回滚信息实践要求:
- CI 使用 Wrapper 和固定 JDK。
- 普通 Pull Request 不获得发布和签名 Secret。
- 远程 Build Cache 对不可信分支默认只读。
- 构件、报告、SBOM、Mapping 和签名都绑定到 Commit 与构建号。
- 发布阶段不重新编译未经验证的源码。
- Release 坐标不可覆盖,回滚通过新版本或仓库提升策略完成。
34.13 Secret 与供应链安全
构建安全的基本原则:
- Secret 只存在于用户级配置、CI Secret 或短期身份凭据中。
- 不在构建脚本、Version Catalog、POM、日志和归档中写入 Secret。
- 仓库在 Settings 中集中声明,并限制允许的 group。
- 依赖使用锁定、校验和或 PGP 验证。
- 插件、Wrapper、Convention Plugin 和外部工具纳入审计。
- 发布账号和缓存账号使用最小权限。
- Fork Pull Request 不使用生产 Secret。
- 发生校验失败时保留证据,不删除验证配置绕过问题。
供应链治理还应记录:
依赖坐标、版本、来源、许可证、哈希、SBOM、漏洞状态、审批人和升级原因34.14 发布与版本管理
发布流程应由明确的版本策略驱动:
- Snapshot 和 Release 使用不同仓库或不同权限。
- Release 版本不可变,不覆盖已上传坐标。
- Git Tag、项目版本、POM、Module Metadata 和构件名称一致。
- sources、javadoc、签名、校验文件和 SBOM 与主构件同步发布。
- Release 前进行独立 Consumer 验证。
- 发布凭据、签名私钥和审批权限不进入普通构建流程。
发布任务示例:
.\gradlew.bat clean check.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository.\gradlew.bat publishMavenJavaPublicationToInternalRepository --dry-run34.15 多项目与依赖方向
多项目依赖图应尽量保持单向和分层:
app / distribution ↓feature modules ↓core implementation ↓api / contracts治理建议:
- 公共接口放在低层模块,避免低层依赖应用模块。
- 使用 Architecture Test 或自定义校验任务检查禁止依赖。
- 只在需要时应用
java-library、Android、Publishing 和质量插件。 - 避免所有模块都依赖根项目的隐式全局变量。
- 通过 Convention Plugin 统一规则,通过模块脚本保留业务差异。
- 对跨模块 API 变更执行兼容性和消费方测试。
34.16 配置文件与团队约定
推荐为项目提供以下文档:
README.mdCONTRIBUTING.mdBUILDING.mddocs/building.mddocs/releasing.mddocs/dependency-upgrades.md至少说明:
- 所需 JDK、SDK、Wrapper 和工具链。
- 本地构建、测试、Lint 和发布命令。
- 依赖升级、锁文件和验证元数据更新流程。
- CI 工作流、缓存、Secret 和权限边界。
- Release、签名、Staging、回滚和事件响应流程。
- 常见问题和最小复现方式。
团队约定应尽量通过自动化检查执行,而不是只写在文档里。例如:
- 用
validatePlugins检查自定义插件。 - 用任务检查仓库、依赖作用域和禁止配置。
- 用 CI 检查锁文件和验证元数据无意外变化。
- 用格式化、Lint 和静态分析阻止低质量变更。
- 用独立 Consumer 验证发布物。
34.17 评审构建变更
审查 Gradle 变更时可以按以下顺序提问:
边界:
- 这项逻辑应该属于 Settings、Plugin、Task 还是模块脚本?
- 是否会影响所有模块、所有变体或所有 CI Job?
输入输出:
- 所有输入是否声明?
- 输出是否位于正确目录?
- 是否引入了时间、随机数、网络或绝对路径?
依赖:
- 依赖作用域是否正确?
- 版本、仓库、锁文件和验证元数据是否合理?
- 是否引入了新的许可证、漏洞或构件体积风险?
性能:
- 是否在配置阶段做了昂贵工作?
- 是否创建了不必要的任务或变体?
- 是否破坏增量构建、Build Cache 或 Configuration Cache?
安全与发布:
- 是否读取或输出 Secret?
- 是否扩大 CI 权限或远程缓存写权限?
- 发布是否可审计、可验证、可回滚?
34.18 常见反模式
反模式一:根脚本承载所有逻辑
症状是根脚本几百行、所有模块通过条件分支配置。应逐步迁移到 Convention Plugin 和清晰的模块边界。
反模式二:配置阶段执行命令
配置阶段调用 Git、网络、编译器或文件扫描会拖慢所有 Gradle 命令,并破坏配置缓存。应移动到任务执行阶段并声明输入。
反模式三:用 clean 解决所有问题
这会掩盖增量构建、生成输出和任务输入问题。应先比较普通构建、无缓存构建和强制重跑结果。
反模式四:依赖版本到处复制
多个模块重复版本字符串会造成升级遗漏。使用 Version Catalog、Platform、约束和锁文件统一治理。
反模式五:把实现依赖全部声明为 api
这会扩大公共 API、消费者编译类路径和升级影响面。只有确实出现在公共 API 中的类型才应使用 api。
反模式六:通过禁用安全校验让发布通过
删除验证元数据、改用不可信仓库或打印 Secret 都会把短期构建问题变成长期供应链风险。
34.19 分阶段落地路线
如果现有项目还没有治理体系,可以按阶段实施:
第一阶段:建立基线
- 提交 Wrapper。
- 固定 JDK 和插件版本。
- 集中声明仓库。
- 建立
check和 CI 最小工作流。 - 保存构建日志和失败报告。
第二阶段:整理构建逻辑
- 使用
register、named和 Provider API。 - 修复自定义 Task 输入输出。
- 将重复逻辑迁移到
build-logic。 - 统一 Version Catalog、Platform 和依赖作用域。
第三阶段:提升效率和安全
- 启用依赖锁定和验证。
- 评估 Build Cache 和 Configuration Cache。
- 补充 SBOM、漏洞和许可证检查。
- 建立发布、签名、Staging 和回滚流程。
第四阶段:建立持续治理
- 记录构建耗时、缓存命中率和失败率。
- 定期升级 Gradle、JDK、插件和依赖。
- 维护兼容矩阵与迁移文档。
- 对构建逻辑、插件和供应链变更执行定期审计。
34.20 最佳实践总检查清单
基础与结构:
- Wrapper、JDK、Toolchain、插件和 SDK 版本明确。
-
settings.gradle.kts清晰定义项目、仓库和构建逻辑边界。 - 模块职责明确,依赖方向可解释。
- 构建、测试、发布和回滚文档完整。
脚本与插件:
- 使用
plugins {}、register、named、configureEach和 Provider API。 - 配置阶段没有网络、编译、外部进程和大规模 I/O。
- 通用规则放在 Convention Plugin 或独立插件中。
- 自定义 Task 输入输出完整并通过验证。
- 插件没有依赖 Gradle 或 AGP 内部 API。
依赖与安全:
- 仓库集中声明并启用内容过滤。
- Version Catalog、Platform、约束和锁文件协同工作。
- 关键依赖和插件启用 Dependency Verification。
- 动态版本、未审查本地 JAR 和不可信仓库受到限制。
- Secret、签名密钥和发布权限采用最小权限。
性能与质量:
- 任务支持增量执行,缓存策略经过测量。
- Configuration Cache 逐步迁移并处理兼容性问题。
- CI 执行测试、Lint、依赖审计、SBOM 和构件验证。
- 构建性能有基线和趋势数据。
- Release 构件可以从 Commit、构建号和哈希追踪。
34.21 代码评审模板
可以把以下内容放入 Pull Request 模板:
## Gradle 变更检查
- [ ] 说明变更属于 Settings、Plugin、Task、依赖还是发布流程- [ ] 使用 Wrapper 和受支持的 JDK 验证- [ ] 未在配置阶段增加网络、I/O、编译或外部进程- [ ] 自定义任务输入输出完整,缓存行为已考虑- [ ] Version Catalog、Platform、锁文件和验证元数据已同步- [ ] 测试、Lint、依赖审计和相关构建任务通过- [ ] 未新增明文 Secret、长期凭据或不可信仓库- [ ] 如涉及发布,已完成 Staging Consumer 和回滚说明- [ ] 如涉及性能,提供前后指标或 Build Scan35. 命令速查表
Gradle 命令的基本结构是:
gradlew[.bat] [任务名称...] [选项...]Windows 项目使用:
.\gradlew.bat <task> [options]Linux 或 macOS 项目使用:
./gradlew <task> [options]命令选项通常可以放在任务前或任务后,但团队脚本最好统一写法。所有示例都优先使用 Wrapper,不依赖开发者机器上的全局 Gradle 安装。
35.1 命令执行前的安全检查
第一次进入项目时,先确认当前目录和 Wrapper:
Get-LocationGet-ChildItem -Force gradlew, gradlew.bat, gradle\wrapper.\gradlew.bat --version确认:
- 当前目录是构建根目录或目标模块所在目录。
gradlew、gradlew.bat和gradle-wrapper.jar来自项目版本库。- 实际 Gradle 版本、JVM 和操作系统符合项目要求。
- 当前命令不会触发远程发布、删除重要目录或使用生产 Secret。
不要在不确认任务来源的情况下直接运行陌生项目的自定义任务。Gradle 构建脚本和插件可以执行任意 JVM 代码,首次运行应先阅读 settings.gradle(.kts)、根构建脚本和 Wrapper 配置。
35.2 环境与版本
# Gradle、Groovy、Kotlin、Launcher、Daemon JVM 和操作系统.\gradlew.bat --version
# 打印版本后继续执行任务.\gradlew.bat --show-version help
# Java 运行环境java -version$env:JAVA_HOME
# 查看 Gradle User Home$env:GRADLE_USER_HOMELinux 或 macOS:
./gradlew --versionjava -versionecho "$JAVA_HOME"echo "$GRADLE_USER_HOME"当本地和 CI 结果不一致时,优先保存这些命令的输出。java -version 显示的是命令行 Java,gradlew --version 还会显示 Gradle 实际使用的 JVM,两者不一定相同。
35.3 帮助与任务发现
# 查看所有常用任务.\gradlew.bat tasks
# 查看所有任务,包括插件注册的隐藏或辅助任务.\gradlew.bat tasks --all
# 查看全局命令行选项.\gradlew.bat --help
# 查看某个任务的详细信息、路径、类型和选项.\gradlew.bat help --task test.\gradlew.bat help --task publish
# 查看项目列表.\gradlew.bat projects
# 查看项目属性.\gradlew.bat properties
# 查看构建逻辑和插件依赖.\gradlew.bat buildEnvironmentPowerShell 中筛选任务:
.\gradlew.bat tasks --all | Select-String -Pattern 'assemble|publish|test|lint'任务路径规则:
:taskName 根项目任务:subproject:taskName 子项目任务例如:
.\gradlew.bat :core:test.\gradlew.bat :app:assembleDebug35.4 任务执行与组合
# 执行一个任务.\gradlew.bat test
# 按任务图执行多个任务.\gradlew.bat clean test jar
# 执行根项目和所有子项目的同名任务.\gradlew.bat test
# 让任务失败后继续执行其他互不依赖的任务.\gradlew.bat test --continue
# 只显示任务计划,不执行任务动作.\gradlew.bat build --dry-run多任务命令并不等于“按书写顺序执行”。Gradle 会根据任务依赖图决定执行顺序,并尽量跳过不需要执行的任务。
35.5 构建、验证与清理
# 编译主代码和测试代码.\gradlew.bat classes testClasses
# 执行项目约定的验证生命周期.\gradlew.bat check
# 生成完整构建产物.\gradlew.bat build
# 删除当前项目的 build 输出.\gradlew.bat clean
# 清理后执行完整构建,适合基线或发布前验证.\gradlew.bat clean build命令选择原则:
- 日常开发优先使用
check或具体目标任务。 - 不要每次都执行
clean build,否则会失去增量构建收益。 - 发布前可以执行一次干净构建,但应同时记录环境和构件哈希。
assemble主要生成构件,check主要执行验证,build通常组合二者,具体关系以项目任务图为准。
35.6 Java/JVM 项目
# Java 编译.\gradlew.bat compileJava.\gradlew.bat compileTestJava.\gradlew.bat classes
# 测试.\gradlew.bat test.\gradlew.bat test --tests "com.example.AppTest".\gradlew.bat test --tests "com.example.*"
# 运行 Application.\gradlew.bat run.\gradlew.bat run --args="--input sample.txt --verbose"
# JAR 和应用分发.\gradlew.bat jar.\gradlew.bat sourcesJar.\gradlew.bat javadocJar.\gradlew.bat installDist.\gradlew.bat distZip.\gradlew.bat distTar单独运行某个测试前,先确认测试任务支持的过滤参数:
.\gradlew.bat help --task test35.7 Kotlin/JVM 项目
# 查看 Kotlin 相关任务.\gradlew.bat tasks --all | Select-String -Pattern 'Kotlin|compileKotlin'
# 常见编译任务.\gradlew.bat compileKotlin.\gradlew.bat compileTestKotlin.\gradlew.bat test多模块项目指定模块:
.\gradlew.bat :service:compileKotlin.\gradlew.bat :service:testKotlin 编译问题排查时,同时保存:
.\gradlew.bat --version.\gradlew.bat buildEnvironment.\gradlew.bat compileKotlin --stacktrace --info35.8 Android 项目
# 查看 Android 模块任务.\gradlew.bat :app:tasks --all
# APK、AAB 和安装.\gradlew.bat :app:assembleDebug.\gradlew.bat :app:assembleRelease.\gradlew.bat :app:bundleRelease.\gradlew.bat :app:installDebug
# 单元测试、设备测试和 Lint.\gradlew.bat :app:testDebugUnitTest.\gradlew.bat :app:connectedDebugAndroidTest.\gradlew.bat :app:lintDebug
# 变体任务示例.\gradlew.bat :app:assembleFreeDebug.\gradlew.bat :app:testPaidReleaseUnitTest.\gradlew.bat :app:signingReport变体名称通常使用 Camel Case 组合 Flavor 和 Build Type。任务不存在时,不要猜名称,先运行:
.\gradlew.bat :app:tasks --all | Select-String -Pattern 'assemble|bundle|test|lint'35.9 多项目构建
# 列出所有项目.\gradlew.bat projects
# 运行指定子项目任务.\gradlew.bat :core:build.\gradlew.bat :app:test
# 运行多个子项目任务.\gradlew.bat :api:check :core:check :app:check
# 查看指定子项目依赖.\gradlew.bat :app:dependencies --configuration runtimeClasspath
# 查看指定子项目对外暴露的变体.\gradlew.bat :core:outgoingVariants根项目任务和子项目任务要区分清楚。build 在根项目执行时,是否会触发所有子项目的 build,取决于根项目的任务依赖关系和插件配置。
35.10 依赖树与冲突诊断
# 查看默认依赖树.\gradlew.bat dependencies
# 查看指定 Configuration.\gradlew.bat dependencies --configuration runtimeClasspath.\gradlew.bat dependencies --configuration testRuntimeClasspath
# 定位一个模块为什么被选中.\gradlew.bat dependencyInsight ` --dependency guava ` --configuration runtimeClasspath
# 查看构建逻辑依赖.\gradlew.bat buildEnvironment
# 查看项目属性和版本信息.\gradlew.bat properties依赖命令常用 Configuration:
| Configuration | 关注内容 |
|---|---|
compileClasspath | 编译时可见依赖 |
runtimeClasspath | 运行时实际依赖 |
testCompileClasspath | 测试编译依赖 |
testRuntimeClasspath | 测试运行依赖 |
debugRuntimeClasspath | Android Debug 运行依赖 |
releaseRuntimeClasspath | Android Release 运行依赖 |
35.11 依赖锁定、验证与缓存刷新
# 生成或更新所有可锁定 Configuration 的锁文件.\gradlew.bat dependencies --write-locks
# 只更新指定模块.\gradlew.bat dependencies --update-locks org.example:example-core
# 使用严格依赖验证.\gradlew.bat --dependency-verification strict clean check
# 生成 SHA-256 验证元数据.\gradlew.bat --write-verification-metadata sha256 build
# 重新检查远程依赖元数据.\gradlew.bat build --refresh-dependencies
# 仅使用本地缓存执行.\gradlew.bat build --offline注意:
--write-locks和--write-verification-metadata会修改项目文件,应在专门的升级分支执行。--refresh-dependencies不会替代锁定和验证。--offline适合验证离线可用性,不适合证明远程仓库内容正确。- 在 CI 中不要允许普通构建自动修改锁文件或验证元数据。
35.12 任务输入输出与增量诊断
# 查看任务是否会执行.\gradlew.bat build --info
# 强制任务重新执行,仅用于诊断.\gradlew.bat build --rerun-tasks
# 关闭 Build Cache 进行对照.\gradlew.bat build --no-build-cache
# 关闭 Configuration Cache 进行对照.\gradlew.bat build --no-configuration-cache
# 只运行任务计划.\gradlew.bat build --dry-run典型判断:
UP-TO-DATE:Gradle 认为输入没有变化且输出有效。FROM-CACHE:任务输出从 Build Cache 恢复。SKIPPED:任务因为条件、无输入或其他规则被跳过。NO-SOURCE:任务没有可处理的输入源。EXECUTED:任务实际执行了动作。
不要把 --rerun-tasks 写进日常脚本。它会破坏增量和缓存诊断价值。
35.13 日志、堆栈与问题报告
# 显示用户异常堆栈.\gradlew.bat build --stacktrace
# 显示完整堆栈.\gradlew.bat build --full-stacktrace
# 更详细的生命周期日志.\gradlew.bat build --info
# 最详细日志,注意脱敏.\gradlew.bat build --debug
# 生成性能报告.\gradlew.bat build --profile
# 生成 Build Scan.\gradlew.bat build --scan
# 显示所有弃用警告.\gradlew.bat help --warning-mode all日志参数选择:
| 参数 | 适用场景 |
|---|---|
--stacktrace | 一般构建失败排查 |
--full-stacktrace | 需要完整异常链 |
--info | 任务、依赖、缓存和配置细节 |
--debug | 深度诊断,输出可能含敏感信息 |
--profile | 初步性能报告 |
--scan | 综合环境、任务、依赖和性能分析 |
--warning-mode all | 升级前处理弃用 API |
35.14 Daemon 与内存
# 查看当前 Gradle Daemon.\gradlew.bat --status
# 停止当前 Gradle 版本启动的 Daemon.\gradlew.bat --stop
# 单次构建不使用常驻 Daemon.\gradlew.bat check --no-daemon
# 单次构建启用 Daemon.\gradlew.bat check --daemon排查内存或 Daemon 问题时:
.\gradlew.bat build --no-parallel --no-build-cache.\gradlew.bat --stop.\gradlew.bat build --stacktraceCI 是否使用 --no-daemon 应由 Runner 生命周期和组织规范决定。不要仅因为一次 OOM 就永久关闭 Daemon,应同时检查 org.gradle.jvmargs、并行度、测试 JVM、Kotlin 编译器和 R8 内存峰值。
35.15 并行、连续构建与缓存
# 单次构建启用并行项目执行.\gradlew.bat build --parallel
# 启用本地 Build Cache.\gradlew.bat build --build-cache
# 显式关闭 Build Cache.\gradlew.bat build --no-build-cache
# 启用 Configuration Cache.\gradlew.bat build --configuration-cache
# 文件变化后自动重新执行相关任务.\gradlew.bat test --continuous这些选项应根据项目和插件兼容性使用:
--parallel适合模块之间依赖较少且任务线程安全的构建。--build-cache需要任务输入输出完整,不能替代任务建模。--configuration-cache需要构建逻辑和插件满足配置缓存要求。--continuous适合本地开发,不适合作为普通 CI 命令。
35.16 Wrapper 管理
# 生成当前 Gradle 版本的 Wrapper 文件.\gradlew.bat wrapper
# 升级到目标 Gradle 版本.\gradlew.bat wrapper --gradle-version 9.6.1
# 使用完整发行包,包含源码和文档.\gradlew.bat wrapper --gradle-version 9.6.1 --distribution-type all
# 指定二进制发行包,适合大多数 CI.\gradlew.bat wrapper --gradle-version 9.6.1 --distribution-type bin升级 Wrapper 后检查:
Get-Content gradle\wrapper\gradle-wrapper.properties.\gradlew.bat --version.\gradlew.bat help在需要完整更新 Wrapper JAR 的场景,按照当前 Gradle Wrapper 文档执行对应的第二次 wrapper 任务,并审查:
gradle-wrapper.jar。gradle-wrapper.properties。gradlew和gradlew.bat。- 发行包 URL、类型、校验和和网络超时。
35.17 项目属性、系统属性与环境变量
# 项目属性,供 build.gradle.kts 使用.\gradlew.bat build -PreleaseVersion=1.2.0
# Gradle JVM 系统属性.\gradlew.bat build -Dorg.gradle.logging.level=info
# 指定本次构建的 JDK.\gradlew.bat build -Dorg.gradle.java.home="C:\Program Files\Java\jdk-21"
# 指定 Gradle User Home.\gradlew.bat build -Dgradle.user.home="$PWD\.gradle-user-home"CI 中可以通过环境变量传递项目属性:
$env:ORG_GRADLE_PROJECT_releaseVersion = "1.2.0".\gradlew.bat build优先级、Secret 和配置缓存需要特别注意:
- 命令行参数通常具有较高优先级。
-P是项目属性,-D是 JVM/系统属性。- 不要把密码写进命令行,因为命令行可能进入日志或进程记录。
- Secret 优先通过 CI Secret 或受保护环境变量注入。
- 构建脚本中使用
providers.gradleProperty()延迟读取属性。
35.18 发布与构件
# 生成发布元数据.\gradlew.bat generatePomFileForMavenJavaPublication.\gradlew.bat generateMetadataFileForMavenJavaPublication
# 发布到项目内临时仓库.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository
# 发布到 Maven Local.\gradlew.bat publishToMavenLocal
# 发布到指定远程仓库.\gradlew.bat publishMavenJavaPublicationToInternalRepository
# 查看发布任务.\gradlew.bat tasks --group publishing
# 只查看发布计划.\gradlew.bat publish --dry-run发布前检查:
.\gradlew.bat clean check.\gradlew.bat sourcesJar javadocJar.\gradlew.bat signMavenJavaPublication不要用 publish 代替构件验证。应先发布到可清理的 Staging 仓库,再用独立 Consumer 验证 POM、Module Metadata、依赖、签名和运行时行为。
35.19 测试与质量任务
# 运行全部验证.\gradlew.bat check
# 运行指定测试.\gradlew.bat test --tests "com.example.AppTest.greetingIsStable"
# 测试失败后继续运行其他测试任务.\gradlew.bat test --continue
# 查看测试任务选项.\gradlew.bat help --task test
# JaCoCo.\gradlew.bat test jacocoTestReport.\gradlew.bat jacocoTestCoverageVerification
# Checkstyle、Spotless 等质量任务.\gradlew.bat checkstyleMain.\gradlew.bat spotlessCheck任务名称由插件决定,项目可能使用不同的质量工具。先运行 tasks --all 或 help --task 确认任务是否存在,不要把某个插件的任务名当成 Gradle 内置命令。
35.20 Android 发布与签名
# Android Debug 验证.\gradlew.bat :app:assembleDebug.\gradlew.bat :app:testDebugUnitTest.\gradlew.bat :app:lintDebug
# Android Release 产物.\gradlew.bat :app:assembleRelease.\gradlew.bat :app:bundleRelease.\gradlew.bat :app:signingReport
# 指定 Flavor 和 Build Type.\gradlew.bat :app:assembleFreeRelease.\gradlew.bat :app:bundlePaidReleaseRelease 命令通常需要签名属性和 CI Secret。不要在本地命令行中直接输入密钥库口令,也不要把签名文件打进公开构建日志或缓存。
35.21 CI 推荐命令组合
Pull Request:
.\gradlew.bat --dependency-verification strict clean check --no-daemon --stacktrace主分支:
.\gradlew.bat clean build --no-daemon --scan.\gradlew.bat dependencies --configuration runtimeClasspathRelease:
.\gradlew.bat clean check.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository.\gradlew.bat publish --no-daemon --stacktrace缓存对照:
.\gradlew.bat clean check --no-build-cache --no-configuration-cacheCI 脚本应固定 Wrapper、JDK、工作目录、权限和 Secret 范围,并上传测试报告、构件、SBOM、签名和构建哈希。
35.22 常见场景命令组合
第一次了解项目:
.\gradlew.bat --version.\gradlew.bat projects.\gradlew.bat tasks --all.\gradlew.bat buildEnvironment.\gradlew.bat dependencies --configuration runtimeClasspath依赖冲突:
.\gradlew.bat dependencyInsight --dependency module-name --configuration runtimeClasspath.\gradlew.bat build --refresh-dependencies --info任务是否执行:
.\gradlew.bat targetTask --dry-run.\gradlew.bat targetTask --info.\gradlew.bat targetTask --rerun-tasks配置缓存问题:
.\gradlew.bat targetTask --configuration-cache.\gradlew.bat targetTask --no-configuration-cache --stacktraceCI 和本地差异:
.\gradlew.bat --version.\gradlew.bat clean check --no-daemon --no-build-cache --stacktrace发布前验证:
.\gradlew.bat clean check.\gradlew.bat generatePomFileForMavenJavaPublication.\gradlew.bat generateMetadataFileForMavenJavaPublication.\gradlew.bat publishAllPublicationsToStagingDirectoryRepository35.23 Windows、Linux 与 macOS 差异
Windows:
.\gradlew.bat clean buildLinux 或 macOS:
./gradlew clean build排查跨平台问题时关注:
- 路径分隔符和大小写敏感性。
- 文件锁和进程释放时间。
- Shell 语法、环境变量和命令搜索路径。
- 换行符、字符集和默认区域设置。
- 外部工具的可执行文件扩展名。
- Docker、CI Runner 和本地 JDK 的文件权限。
构建逻辑中应优先使用 Gradle File API 和 Provider API,避免把 PowerShell 或 Bash 特定语法硬编码到跨平台任务中。
35.24 命令使用原则
- 先用
--help、help --task和tasks --all确认命令语义。 - 先执行最小范围任务,再扩大到
check、build和发布流程。 - 临时诊断选项不要写入项目永久配置。
clean、--rerun-tasks、--refresh-dependencies和关闭缓存都应有明确原因。- 使用
--debug、--scan和发布命令前确认日志与权限边界。 - 远程发布、签名和修改锁文件的命令必须在受控环境执行。
- 命令结果要结合任务图、日志、构建报告和构件检查,而不是只看退出码。
35.25 脚本化执行与返回码
在 PowerShell、Bash 或 CI 中调用 Gradle 时,应把 Gradle 的退出码作为脚本成功与否的主要依据。不要通过 Out-Null、重定向或字符串搜索吞掉失败。
PowerShell:
$ErrorActionPreference = "Stop"
& .\gradlew.bat clean check --no-daemon --stacktraceif ($LASTEXITCODE -ne 0) { throw "Gradle verification failed with exit code $LASTEXITCODE"}Bash:
set -Eeuo pipefail
./gradlew clean check --no-daemon --stacktrace脚本化命令还应注意:
- 使用
--no-daemon、--stacktrace等参数时,要结合 CI Runner 生命周期和日志策略。 - 需要传递项目属性时,优先使用受保护环境变量映射,而不是把 Secret 写在命令行。
- 上传报告和构件应放在
always()或等价的失败后步骤中,但不能因此把 Gradle 失败标记为成功。 - 发布、签名、锁文件更新和验证元数据写入应拆成独立、受保护的 Job。
- 组合多个任务时,先确认任务图和失败传播行为,不要用
;或忽略错误强行继续。
例如,发布前可以把验证和发布拆开:
& .\gradlew.bat clean check --no-daemon --stacktraceif ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE}
& .\gradlew.bat publishAllPublicationsToStagingDirectoryRepository --no-daemonif ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE}36. 参考资料
本文优先参考以下官方资料,快速变化的版本内容应以链接目标为准:
- Gradle User Manual
- Gradle Releases
- Gradle Compatibility Matrix
- Gradle Build Lifecycle
- Gradle Wrapper
- Task Configuration Avoidance
- Declaring Dependencies
- Dependency Management
- Version Catalogs
- Platforms
- Dependency Constraints
- Dependency Locking
- Dependency Verification
- Multi-project Builds
- Composite Builds
- Sharing Build Logic
- Incremental Build
- Build Cache
- Configuration Cache
- Build Environment
- Testing in Java & JVM Projects
- Maven Publish Plugin
- Android Build Documentation
- Android Gradle Plugin Release Notes
37.1 核心概念与命令
- Command-Line Interface:命令行参数、日志级别、任务排除和调试选项。
- Build Init Plugin:使用
gradle init初始化项目。 - Gradle User Home:了解项目目录、用户目录和缓存位置。
- Gradle DSL Reference:查询 Gradle DSL 类型、属性和方法。
- Gradle API Javadoc:查询插件和自定义 Task 使用的 API。
- Gradle Glossary:Build、Project、Task、Configuration 等术语定义。
- Gradle Basics:从项目、任务、插件和依赖开始理解 Gradle。
- Build Lifecycle:初始化、配置、执行三个构建阶段。
- More About Tasks:任务依赖、任务顺序、跳过和任务图基础。
- Authoring Maintainable Build Scripts:组织可维护的构建脚本和避免隐式耦合。
- Organizing Gradle Projects:组织单项目、多项目和构建逻辑。
37.2 DSL、插件与构建逻辑
- Kotlin DSL Primer:Kotlin DSL 的语法、类型安全访问器和脚本组织方式。
- Groovy Build Script Primer:Groovy DSL 基础和历史项目迁移参考。
- Plugins User Manual:插件应用、插件解析和插件版本管理。
- Plugin Management:配置插件仓库和插件解析规则。
- Implementing Binary Plugins:实现二进制 Gradle Plugin。
- Precompiled Script Plugins:使用 Kotlin DSL 编写预编译脚本插件。
- Sharing Build Logic Between Subprojects:使用
buildSrc和 Included Build 复用约定。 - Convention Plugins:将组织级构建约定提取为插件。
- Build Services:在任务之间安全共享有限生命周期的服务。
- Worker API:并行执行隔离的工作单元。
37.3 Java、Kotlin 与测试
- Java Plugin:Java Source Set、编译、测试和 JAR 任务。
- Java Library Plugin:
api、implementation和库变体。 - Java Toolchains:统一编译、测试和运行使用的 JDK。
- Application Plugin:Java 应用运行和分发包。
- Kotlin Gradle Plugin Documentation:Kotlin/JVM、Kotlin Multiplatform 与 Gradle 集成。
- Kotlin Compiler Options:Kotlin 编译器目标和编译参数。
- Testing in Java & JVM Projects:JUnit、TestNG、测试过滤和报告。
- JUnit User Guide:JUnit Jupiter、平台和测试引擎。
- TestNG Documentation:TestNG 测试框架和测试配置。
- JaCoCo Plugin:Java 测试覆盖率报告。
- Checkstyle Plugin:Java 代码风格检查。
37.4 依赖、仓库与变体
- Declaring Repositories:Maven、Ivy、私有仓库和仓库内容过滤。
- Dependency Resolution Rules:替换、排除、强制版本和解析规则。
- Viewing and Debugging Dependencies:
dependencies和dependencyInsight。 - Variant Model:变体、属性匹配和能力选择。
- Component Metadata Rules:修正或补充外部模块元数据。
- Capabilities:处理多个组件提供同一能力的情况。
- Centralizing Repository Declarations:在 Settings 中统一仓库和仓库模式。
- Version Catalogs:
libs.versions.toml、别名和依赖 bundle。 - Platforms:Gradle Platform、BOM 和版本对齐。
- Dependency Constraints:表达版本约束和兼容范围。
- Dependency Locking:固定解析结果并审查依赖升级。
- Dependency Verification:使用校验和或签名验证依赖。
- Gradle Module Metadata:发布和消费 Gradle 变体元数据。
- Maven Central Repository:查询公开 Maven 构件坐标和版本。
37.5 多项目、发布与性能
- Multi-Project Builds:根项目、子项目和项目依赖。
- Composite Builds:使用 Included Build 组合独立构建。
- Sharing Build Logic:跨模块复用构建逻辑。
- Maven Publish Plugin:发布 POM、Module Metadata、sources 和 javadoc。
- Signing Plugin:对发布构件进行签名。
- Incremental Build:声明任务输入输出并支持增量执行。
- Task Configuration Avoidance:使用
register、named和configureEach。 - Build Cache:本地和远程任务输出缓存。
- Configuration Cache:缓存构建配置结果。
- Build Cache Samples:官方构建缓存示例。
- Build Scan:分析构建性能、任务状态和依赖解析。
- Gradle Performance:配置、执行、缓存和并行性能优化。
37.6 安全、环境与持续集成
- Build Environment:
gradle.properties、环境变量和命令行属性。 - Gradle Wrapper:固定 Gradle 版本和校验分发包。
- Gradle Wrapper Validation Action:在 CI 中校验 Wrapper JAR。
- Gradle Build Action:在 GitHub Actions 中配置 Gradle 缓存和执行环境。
- GitHub Actions Documentation:工作流、Secret、缓存和构件管理。
- GitHub Actions
setup-java:在 CI 中配置 JDK 和依赖缓存。 - GitLab CI/CD Documentation:GitLab Runner、流水线、缓存和构件管理。
- Dependency Verification:依赖来源、校验和与签名验证。
- Gradle Security:Gradle 产品和生态安全公告。
- Gradle Build Tool Vulnerability Disclosure:漏洞披露和安全报告渠道。
37.7 Android 构建
- Android Build Documentation:Android Gradle 构建总览。
- Android Gradle Plugin Release Notes:AGP 版本变化和升级说明。
- Android Gradle Plugin API Reference:Android 构建 DSL 和插件 API。
- Android Build Variants:Build Type、Product Flavor 和 Variant。
- Android Manage Dependencies:Android 依赖声明、仓库和版本治理。
- Android Build Configuration:Android 构建配置和 APK 拆分。
- Android Lint:Android 静态分析和质量检查。
- Android Testing:本地测试、仪器测试和测试策略。
- R8 and Code Shrinking:代码压缩、混淆和资源优化。
- Android Developers Samples:Android 官方示例仓库。
37.8 对比工具与扩展生态
- Apache Maven Guides:理解 Maven 生命周期、依赖和发布模型。
- Apache Ivy Documentation:了解 Ivy 仓库和依赖管理模型。
- Gradle Plugin Portal:查询公开 Gradle Plugin 和版本。
- Gradle Community Plugins:了解社区插件使用边界。
- Gradle Samples:官方文档示例代码。
- Gradle Build Tool Source Code:查看 Gradle 实现、Issue 和变更记录。
37.9 Ant、Maven 与迁移
- Apache Ant Manual:Ant 项目、Target、Task、Property 和构建文件基础。
- Ant Targets:Target 的依赖、执行条件和组织方式。
- Ant Tasks Overview:Ant 内置 Task 和扩展 Task 总览。
- Ant Properties:Property 定义、覆盖和使用规则。
- Apache Ivy Documentation:Ivy 依赖管理和仓库模型。
- Maven POM Reference:POM 元素、坐标、属性和依赖声明。
- Maven Build Lifecycle:Maven 生命周期、Phase 和 Plugin Goal。
- Maven Dependency Mechanism:Scope、传递依赖和依赖管理。
- Maven Standard Directory Layout:Maven 默认源码、资源和测试目录。
- Maven Multiple Modules:Maven 多模块 Reactor 构建。
- Maven Plugin Development:编写 Maven Plugin 和 Mojo。
- Gradle Migrating from Maven:从 Maven 项目迁移到 Gradle 的官方指导。
- Using Ant from Gradle:在 Gradle 中调用 Ant Task 和 Ant 构建逻辑。
- Gradle vs Maven:Gradle 与 Maven 的模型、性能和使用场景对比。
- Maven Plugin Portal:Maven 官方插件目录和插件文档。
37.10 环境、安装与 JVM
- Installing Gradle:全局安装 Gradle、发行包和安装验证。
- Gradle Releases:查看 Gradle 版本、发行说明和分发包。
- Gradle Compatibility Matrix:核对 Gradle 与 JVM、插件和环境的兼容范围。
- Gradle Wrapper:项目级 Gradle 版本固定、分发包和校验。
- Gradle Build Environment:
JAVA_HOME、org.gradle.java.home、属性和环境变量。 - Gradle Directory Layout:项目目录、Gradle User Home、缓存和初始化脚本。
- Gradle Daemon:Daemon 生命周期、状态、停止和 JVM 参数。
- Java Toolchains:编译、测试和运行任务的 JDK 选择。
- SDKMAN! Installation:在 Linux/macOS 中管理 JDK 和 Gradle 版本。
- Homebrew Gradle Formula:通过 Homebrew 安装 Gradle。
- Scoop:Windows 命令行包管理器及其 Gradle 包。
- Eclipse Temurin:常用 OpenJDK 发行版,可用于本地和 CI 环境。
37.11 Wrapper 专项资料
- Gradle Wrapper User Guide:Wrapper 的生成、升级、使用和提交策略。
- Gradle Wrapper Plugin:Wrapper 任务和配置 API。
- Gradle Wrapper Properties:
gradle-wrapper.properties的分发、缓存和网络属性。 - Gradle Distribution Checksums:核对 Gradle 分发包 SHA-256 校验值。
- Gradle Releases:查看版本、发行说明和分发包入口。
- Gradle Actions Wrapper Validation:使用官方 GitHub Action 检查 Wrapper JAR。
- Gradle Wrapper Validation Documentation:配置 Wrapper 验证工作流和使用方式。
37.12 项目初始化与模板
- Build Init Plugin:
gradle init、项目模板、参数和初始化流程。 - Gradle Command-Line Interface:命令行选项、任务参数、日志和非交互式执行。
- Settings File Basics:
settings.gradle(.kts)、根项目和子项目定义。 - Plugin Basics:项目插件、插件版本和插件应用方式。
- Organizing Gradle Projects:项目目录、单项目和多项目组织。
- Java Plugin:Java 项目默认 Source Set、编译和测试任务。
- Java Library Plugin:Library 项目、API 暴露和发布变体。
- Application Plugin:Application 项目、
run任务和分发包。 - Kotlin DSL Primer:Kotlin DSL 项目脚本和类型安全访问器。
- Groovy Build Script Primer:Groovy DSL 项目脚本。
- Gradle Samples:Gradle 官方示例项目和最小构建样例。
37.13 项目结构与构建文件
- Directory Layout:项目目录、Gradle User Home、缓存和初始化脚本。
- Settings File Basics:根项目、子项目和 Build 边界。
- Writing Build Scripts:构建脚本结构、Project API 和脚本组织。
- Build Environment:
gradle.properties、系统属性、项目属性和环境变量。 - Initialization Scripts:用户级和机器级 Init Script 的加载顺序及使用方式。
- Java Plugin Source Sets:
main、test、源码目录和 Source Set 配置。 - Incremental Build:任务输入输出、生成目录和增量构建。
- Multi-Project Builds:根项目、子项目、项目路径和跨模块依赖。
- Sharing Build Logic:使用
buildSrc和 Included Build 组织构建逻辑。 - Android Project Structure:Android 模块、Source Set、
local.properties和构建输出。
37.14 Kotlin DSL 与 Groovy DSL
- Kotlin DSL Primer:Kotlin DSL 语法、脚本模型、类型安全访问器和最佳实践。
- Kotlin DSL API Documentation:Kotlin DSL API、Extension、Property 和 Provider 类型。
- Kotlin DSL Migration Guide:从 Groovy DSL 迁移到 Kotlin DSL。
- Type-Safe Model Accessors:插件、Extension、Configuration 和 Task 的类型安全访问器。
- Groovy Build Script Primer:Groovy DSL、Closure 和构建脚本委托对象。
- Precompiled Script Plugins:使用 Kotlin/Groovy 脚本编写可复用 Convention Plugin。
- Implementing Binary Plugins:用 Kotlin 或 Groovy 实现二进制 Gradle Plugin。
- Authoring Maintainable Build Scripts:构建脚本组织、懒配置和可维护性原则。
- Task Configuration Avoidance:
register、named和configureEach的懒配置 API。 - Lazy Configuration:Property、Provider 和延迟配置模型。
- Gradle API Javadoc:构建脚本和自定义插件使用的 Gradle API。
37.15 构建生命周期与任务图
- Build Lifecycle:Initialization、Configuration、Execution 三个阶段。
- More About Tasks:Task、任务依赖、任务顺序和生命周期任务。
- Task Graph:任务图构建、任务选择和依赖关系。
- Task Configuration Avoidance:避免配置阶段创建和配置不必要的 Task。
- Lazy Configuration:Provider、Property 和延迟求值。
- Incremental Build:任务输入输出、up-to-date 检查和增量执行。
- Build Cache:从本地或远程缓存恢复任务输出。
- Configuration Cache:缓存配置阶段和任务图结果。
- Build Environment:影响生命周期和配置结果的属性、环境变量和 JVM 参数。
- Gradle Daemon:Daemon、配置复用和构建进程诊断。
- Build Scan:查看任务状态、配置耗时、依赖解析和执行详情。
37.16 Task、输入输出与自定义任务
- More About Tasks:Task 注册、配置、依赖、顺序和执行状态。
- Custom Tasks:定义自定义 Task 类型、Task Action 和任务属性。
- Task Input and Output Annotations:
@Input、@OutputFile、@InputFiles等声明。 - Task Output Caching:可缓存 Task、输入快照和缓存命中。
- Task Configuration Avoidance:懒注册和懒配置任务。
- Task Provenance:诊断任务由哪个插件或构建逻辑注册。
- Exec Task DSL:执行外部进程的
ExecTask。 - Copy Task DSL:复制文件和目录的
CopyTask。 - Build Services:Task 之间共享有限生命周期资源。
- Worker API:将 Task 工作拆分为隔离的并发单元。
- Gradle TestKit:通过真实 Gradle 执行测试自定义 Task 和 Plugin。
- Writing Tasks:Task Action、
doFirst、doLast和任务动作顺序。
37.17 插件机制与插件开发
- Plugins User Manual:插件应用、插件 ID、版本和插件管理。
- Plugin Basics:插件解析、Plugin Marker 和应用方式。
- Plugin Management:插件仓库、解析规则和内部插件映射。
- Plugin Resolution Rules:使用
resolutionStrategy映射插件请求。 - Precompiled Script Plugins:实现 Convention Plugin 和预编译脚本插件。
- Implementing Binary Plugins:使用 Kotlin、Java 或 Groovy 编写二进制插件。
- Java Gradle Plugin Development Plugin:创建、测试和发布 Gradle Plugin 项目。
- Gradle Plugin Development:插件 Extension、Task、约定和实现边界。
- Sharing Build Logic:
buildSrc、Included Build 和build-logic。 - Gradle TestKit:使用真实 Gradle 构建验证插件行为。
- Publishing Gradle Plugins:插件 Marker、发布仓库和插件版本。
- Plugin Portal:查询公开插件、版本和插件文档。
- Gradle Plugin Development Samples:Gradle 官方插件开发示例。
使用第三方插件时,应同时查看插件自己的官方仓库、版本说明、兼容矩阵和安全记录。插件门户上的存在性不等于插件适合直接用于生产项目。
37.18 Java、Kotlin/JVM 与 JVM 测试
- Java Plugin:Java Source Set、编译、资源、测试和 JAR 任务。
- Java Library Plugin:
api、implementation、库变体和测试夹具协作。 - Application Plugin:入口类、
run任务、启动脚本和应用分发包。 - Java Toolchains:为编译、测试、Javadoc 和运行任务选择 JDK。
- Java Testing:JUnit、TestNG、测试过滤、日志和测试报告。
- JVM Test Suite Plugin:声明式配置单元测试和集成测试套件。
- Java Test Fixtures Plugin:在多个模块之间复用测试夹具。
- Maven Publish Plugin:发布 Java 组件、POM、模块元数据和仓库凭据。
- JavaCompile DSL:Java 编译任务、编码和编译器参数。
- Kotlin Gradle Plugin Documentation:Kotlin/JVM 与 Gradle 集成、Toolchain 和项目配置。
- Kotlin Compiler Options:Kotlin 编译器选项、语言版本和 JVM 目标。
- Mixed Kotlin and Java Projects:Kotlin 与 Java 源码混合编译的项目约束。
37.19 依赖管理、解析与供应链
- Declaring Dependencies:直接依赖、项目依赖、文件依赖和依赖约束。
- Dependency Management:依赖声明、解析、缓存和治理的整体模型。
- Declaring Repositories:Maven、Ivy、私有仓库和仓库内容过滤。
- Centralizing Repository Declarations:在
settings.gradle.kts中统一仓库声明和仓库模式。 - Version Catalogs:
libs.versions.toml、别名、版本和依赖 bundle。 - Platforms:
java-platform、Maven BOM 和版本对齐。 - Dependency Constraints:
strictly、require、prefer、reject和约束原因。 - Resolution Rules:依赖替换、排除、强制版本和组件选择规则。
- Viewing and Debugging Dependencies:
dependencies、dependencyInsight和依赖诊断。 - Variant Model:变体、属性匹配、Configuration 和组件选择。
- Component Metadata Rules:修正外部模块元数据和补充依赖信息。
- Capabilities:处理多个组件提供同一能力时的选择和冲突。
- Dependency Locking:记录解析结果、更新锁文件和控制升级范围。
- Dependency Verification:使用校验和、签名和严格模式验证构件。
- Gradle Module Metadata:发布和消费 Gradle 变体元数据。
37.20 Configuration、属性与变体
- Creating Dependency Configurations:区分可声明、可解析和可消费的 Configuration,并使用
extendsFrom组织依赖继承。 - Variant Model:变体、消费者属性、生产者能力和匹配模型。
- Variant Selection and Attribute Matching:兼容性判断、消歧选择和变体匹配流程。
- Variant Attributes:标准属性、属性兼容性和变体消歧规则。
- Custom Compatibility Rules:自定义 Attribute Compatibility Rule。
- Custom Disambiguation Rules:自定义 Attribute Disambiguation Rule。
- Capabilities:声明组件能力并处理 capability 冲突。
- Feature Variants:为 Java Library 创建可选功能变体。
- Outgoing Variants:查看项目对外暴露的 Configuration、属性和构件。
- Sharing Outputs Between Projects:通过项目依赖和变体安全共享跨项目产物。
- Viewing and Debugging Dependencies:使用
dependencies、dependencyInsight和outgoingVariants诊断解析结果。
37.21 版本冲突与依赖诊断
- Viewing and Debugging Dependencies:使用
dependencies、dependencyInsight和依赖报告分析选择结果。 - Resolution Rules:依赖替换、组件选择、强制版本和解析规则。
- Dependency Constraints:使用约束表达版本要求、原因和兼容范围。
- Platforms:使用 Gradle Platform 和 BOM 对齐一组模块版本。
- Dependency Locking:固定解析结果、精确更新锁文件和审查升级。
- Dependency Verification:使用校验和、签名和严格模式验证构件。
- Component Metadata Rules:修正外部模块元数据和补充依赖信息。
- Capabilities:处理多个组件提供同一能力时的冲突和选择。
- Variant-Aware Resolution:理解消费者属性、生产者变体和属性匹配过程。
- Variant Attributes:兼容性规则、消歧规则和自定义属性。
- Gradle Module Metadata:理解变体、依赖约束和组件元数据的发布与消费。
- Dependency Management:依赖解析、缓存、仓库和可复现构建的整体说明。
37.22 Version Catalog、Platform 与版本约束
- Version Catalogs:TOML 目录、别名、bundle、插件别名、
buildSrc和目录发布。 - Centralizing Catalogs and Platforms:Version Catalog 与 Platform 的职责分工及组合方式。
- The Java Platform Plugin:
java-platform、API/Runtime 约束、BOM 导入和 Platform 发布。 - Platforms:使用 Platform、BOM 和
enforcedPlatform进行版本对齐。 - Dependency Constraints:版本约束、约束原因和多项目治理。
- Declaring Versions and Ranges:
strictly、require、prefer、reject和版本范围。 - Dependency Locking:固定解析结果并精确更新锁文件。
- Dependency Verification:校验和、签名和严格依赖验证。
- Publishing a Version Catalog:使用
version-catalog插件发布和消费共享目录。 - Publishing Gradle Module Metadata:发布约束、变体和版本管理元数据。
37.23 仓库管理与供应链
- Declaring Repositories:声明 Maven、Ivy、Flat Directory 和本地仓库。
- Repository Types:仓库类型、Maven 元数据、本地仓库和认证仓库。
- Centralizing Repository Declarations:在 Settings 中统一仓库和仓库模式。
- Filtering Repository Content:内容过滤和 exclusive content。
- Supported Repository Protocols:HTTP(S)、认证方式、凭据和其他仓库协议。
- Plugin Management:插件仓库、插件解析规则和插件版本管理。
- Plugin Basics:插件请求、Plugin Marker 和插件应用方式。
- Dependency Caching:依赖缓存、
--offline、--refresh-dependencies和 changing dependencies。 - Dependency Verification:校验和、签名和严格验证。
- Dependency Locking:固定解析结果并审查仓库或版本变化。
- Publishing Gradle Plugins:发布插件、Plugin Marker 和插件仓库消费。
37.24 多项目构建、Composite Build 与共享构建逻辑
- Multi-Project Builds:根项目、子项目、项目路径和项目依赖。
- Structuring Multi-Project Builds:多项目目录、Settings 和模块组织方式。
- How to Share Artifacts Between Projects:通过变体感知模型共享跨项目构件。
- Composite Builds:Included Build、源码替换和独立构建组合。
- Sharing Build Logic:使用
buildSrc、build-logic和 Convention Plugin 复用构建逻辑。 - Convention Plugins:实现和应用预编译脚本 Convention Plugin。
- Best Practices for Structuring Builds:多项目、构建逻辑和可维护性建议。
- How to Convert a Single-Project Build:从单项目迁移到多项目构建。
- Multi-Project Configuration and Execution:多项目配置、执行和按需配置相关行为。
- Configuration Cache:缓存多项目构建的配置结果。
- Isolated Projects:项目隔离、跨项目访问约束和大型构建性能。
- Task Configuration Avoidance:多项目中的懒注册、懒配置和任务图优化。
- Build Cache:跨项目复用任务输出和构建缓存。
37.25 构建逻辑复用与插件开发
- Sharing Build Logic Between Subprojects:使用
buildSrc、Included Build 和 Convention Plugin 复用构建逻辑。 - Precompiled Script Plugins:使用 Kotlin DSL 或 Groovy DSL 编写预编译脚本插件。
- Convention Plugins:提取组织级构建约定并提供可配置 Extension。
- Implementing Binary Plugins:使用 Java、Kotlin 或 Groovy 实现二进制插件。
- Custom Plugins:插件 Extension、Task、约定和公开 API 设计。
- Java Gradle Plugin Development:使用
java-gradle-plugin创建、测试和发布插件。 - Gradle TestKit:通过真实 Gradle 构建测试插件行为。
- Plugin Development Basics:插件 ID、Plugin Marker、插件请求和应用方式。
- Publishing Gradle Plugins:发布实现模块、Plugin Marker 和插件元数据。
- Gradle Plugin Development Samples:Gradle 官方插件开发示例。
- Lazy Configuration:使用 Provider API、Property 和延迟配置构建逻辑。
- Task Configuration Avoidance:使用
register、named和configureEach编写高效插件。
37.26 Provider、Property 与懒配置
- Lazy Configuration:Provider、Property、
map、flatMap、zip和延迟求值。 - Task Configuration Avoidance:
register、named、configureEach和避免 eager configuration。 - Custom Tasks:自定义 Task、属性、任务动作和任务类型。
- Task Input and Output Annotations:
@Input、@OutputFile、@InputFiles和路径敏感性。 - Incremental Build:任务输入、输出、up-to-date 检查和增量执行。
- Build Cache:基于任务输入输出复用本地和远程缓存。
- Configuration Cache:缓存配置阶段和任务图结果。
- Build Services:在任务之间安全共享有限生命周期资源。
- Worker API:将任务工作拆分为隔离的并发单元。
- Build Environment:项目属性、系统属性、环境变量和 Gradle 参数。
- More About Tasks:任务注册、配置、依赖、顺序和执行状态。
37.27 增量构建、任务输入输出与 Build Cache
- Incremental Build:任务输入输出、快照、up-to-date 检查和增量执行。
- Task Input and Output Annotations:
@Input、@InputFiles、@OutputFile、@Classpath和路径敏感性。 - Custom Tasks:实现自定义 Task 类型、任务动作和属性。
- Task Output Caching:
@CacheableTask、缓存键和可缓存任务要求。 - Build Cache:本地缓存、远程缓存、缓存配置和任务输出复用。
- Build Cache Debugging:诊断缓存未命中、输入变化和缓存调试日志。
- Build Cache Configuration:配置本地缓存、远程缓存、推送策略和凭据。
- Task Configuration Avoidance:懒注册、懒配置和任务图优化。
- Lazy Configuration:Provider、Property 和延迟求值。
- Configuration Cache:缓存配置阶段和任务图结果。
- Build Cache Samples:Gradle 官方构建缓存示例。
37.28 Build Cache 专题资料
- Build Cache:本地缓存、远程缓存、任务输出缓存和缓存生命周期。
- Task Output Caching:
@CacheableTask、缓存键和可缓存任务契约。 - Build Cache Configuration:配置本地缓存、远程缓存、推送策略和缓存凭据。
- Build Cache Debugging:分析缓存未命中、输入变化和缓存调试日志。
- Build Cache Recipes:使用 Build Cache 的构建配置和实践示例。
- Incremental Build:增量任务输入输出、快照和 up-to-date 检查。
- Custom Tasks:自定义任务、输入输出属性和任务动作。
- Task Configuration Avoidance:懒注册和懒配置任务。
- Configuration Cache:缓存配置阶段并减少多次构建的配置成本。
- Build Environment:使用 Gradle 属性和环境变量配置构建缓存。
- Gradle Performance:评估缓存、并行执行和构建性能。
37.29 Configuration Cache 专题资料
- Configuration Cache:配置缓存的工作方式、启用方式、限制和迁移要求。
- Configuration Cache Requirements:Provider、任务输入、执行阶段访问和可序列化配置要求。
- Configuration Cache Troubleshooting:问题报告、兼容性问题和排错方法。
- Configuration Cache Report:查看配置缓存问题报告和问题来源。
- Build Services:在任务之间安全共享有限生命周期资源。
- Lazy Configuration:Provider、Property 和延迟读取外部值。
- Task Configuration Avoidance:减少配置阶段的任务实例化和模型访问。
- Isolated Projects:项目隔离和多项目构建中的跨项目访问约束。
- Build Environment:Gradle 属性、系统属性、环境变量和构建参数。
- Gradle Performance:分析配置阶段、任务执行和缓存性能。
37.30 Daemon、性能与并行构建
- Gradle Daemon:Daemon 生命周期、兼容条件、JVM 参数和性能收益。
- Best Practices for Performance:配置缓存、任务配置规避和性能优化原则。
- Improve the Performance of Gradle Builds:配置阶段、依赖、任务执行和缓存性能分析。
- Inspecting Builds:Build Scan、本地 profile 报告和低级性能分析。
- Build Scan Basics:构建扫描的创建、共享、性能视图和数据策略。
- Parallel Project Execution:多项目并行执行和任务隔离要求。
- Worker API:并行处理隔离工作单元和控制资源使用。
- Task Configuration Avoidance:避免 eager configuration 和不必要的任务实例化。
- Build Cache Performance:评估缓存命中、缓存传输和构建性能。
- Build Environment:JVM 参数、Daemon、并行和构建环境配置。
- Command-Line Interface:
--profile、--scan、--info、--debug和任务诊断参数。
37.31 测试、覆盖率与代码质量
- Testing in Java & JVM Projects:JUnit、TestNG、测试过滤、日志和测试报告。
- JVM Test Suite Plugin:声明式配置 JVM 单元测试和集成测试套件。
- Test Report Aggregation Plugin:聚合多项目测试报告。
- JaCoCo Plugin:生成 JaCoCo 覆盖率报告。
- JaCoCo Report Aggregation:聚合多项目测试覆盖率数据。
- Checkstyle Plugin:配置 Java 代码风格检查和报告。
- Java Test Fixtures Plugin:在项目之间复用测试夹具。
- Test Logging:测试事件、异常格式和标准输出日志。
- Test Filtering:使用
--tests和构建脚本过滤测试。 - Verification Lifecycle:使用
check组织编译、测试和质量验证任务。
37.32 打包、发布与签名
- Java Plugin:Java Source Set、
jar生命周期、归档和 Java 项目构建任务。 - Java Library Plugin:
api、implementation、库组件和可发布变体。 - Working With Files:归档任务、文件集合和可复现归档配置。
- Application Plugin:
run、installDist、distZip、distTar和应用启动配置。 - Distribution Plugin:自定义应用分发内容、目录布局和分发归档。
- Maven Publish Plugin:创建
MavenPublication、配置仓库并发布 Maven 构件。 - Publishing Setup:从软件组件创建 Publication、发布任务和本地仓库演练。
- Customizing Publishing:自定义 POM、附加构件、版本映射和发布内容。
- Gradle Module Metadata:发布
.module元数据、变体、能力和丰富版本信息。 - Supported Repository Protocols:HTTP(S) 仓库认证、
PasswordCredentials和外部凭据属性。 - Signing Plugin:签名 Publication、使用 PGP 密钥以及在 CI 中进行内存签名。
- Publishing Plugins:Gradle 插件与插件 Marker 的发布方式,可用于扩展发布治理。
37.33 依赖锁定与供应链安全
- Dependency Management Basics:依赖声明、仓库、Configuration 和依赖解析基础。
- Dependency Locking:生成、更新和提交依赖锁文件,固定 Configuration 的解析结果。
- Dependency Verification:使用校验和、PGP 签名和
verification-metadata.xml验证依赖来源。 - Declaring Dependencies:直接依赖、传递依赖、依赖作用域和模块声明。
- Dependency Constraints:使用约束表达版本、原因和传递依赖治理规则。
- Platforms:Java Platform、BOM、版本对齐和依赖约束组合。
- Version Catalogs:通过
libs.versions.toml集中管理版本、别名和依赖坐标。 - Centralizing Repository Declarations:在
settings.gradle.kts中集中声明项目仓库。 - Filtering Repository Content:普通内容过滤、独占内容和仓库解析范围限制。
- Supported Repository Protocols:仓库协议、认证方式和外部凭据配置。
- Dependency Caching:依赖缓存、离线构建、缓存刷新和缓存失效策略。
- Dependency Insight Report:使用
dependencyInsight分析版本选择和依赖来源。 - Viewing and Debugging Dependencies:依赖树、冲突、约束、变体和解析结果排查。
- Gradle Wrapper:固定 Gradle 运行版本、升级 Wrapper 和管理 Wrapper 文件。
- Securing Gradle Builds:构建环境、凭据、插件、仓库和构建逻辑安全实践。
- Gradle Best Practices:依赖声明、版本治理和可维护构建的推荐实践。
- CycloneDX Gradle Plugin:使用 CycloneDX 生成 Gradle 项目 SBOM 的官方项目仓库。
- CycloneDX Specification:CycloneDX SBOM 格式、组件、服务和依赖关系规范。
- SPDX Specification:SPDX 软件包数据交换格式和许可证表达规范。
37.34 Android Gradle 构建
- Android Build Overview:Android 构建系统、AGP、Gradle 和 Android 项目构建概览。
- Configure Your Build:配置 Android 项目、模块、插件、SDK 和构建选项的官方入口。
- Android Gradle Plugin Release Notes:AGP 版本变化、迁移说明和兼容性信息。
- AGP and Android Studio Compatibility:Android Studio、AGP、Gradle 和 JDK 的兼容关系。
- Gradle Plugin User Guide:Android Gradle Plugin 的安装、配置和升级指南。
- Build Variants:Build Type、Product Flavor、Flavor Dimension 和 Build Variant。
- Configure Build Types:Debug、Release、自定义 Build Type 和变体属性。
- Configure Product Flavors:Product Flavor、Flavor Dimension、Source Set 和变体组合。
- Configure App Module:App 模块、
applicationId、版本信息和应用构建配置。 - Configure Library Module:Android Library 模块、AAR 和库发布配置。
- Manage Your App’s Dependencies:AndroidX、Google Maven、依赖声明、版本和依赖排查。
- Manifest Merge:Manifest 合并优先级、冲突报告和
tools合并标记。 - Manage Manifest Files:Android Manifest 配置、合并和变体相关清单管理。
- Shrink, Obfuscate, and Optimize Your App:R8、代码压缩、资源收缩、Keep 规则和 Mapping 文件。
- Sign Your App:APK/AAB 签名、密钥管理、上传密钥和发布密钥。
- Build Your App from the Command Line:使用 Gradle Wrapper 构建、测试和安装 Android 应用。
- Configure the Build Environment:Android Studio、Gradle、AGP、JDK 和 Java Toolchain 环境配置。
- Optimize Your Build Speed:Android 构建性能、缓存、并行和配置优化。
- Android Gradle Plugin API:Android Components 和公开变体 API 的参考文档。
- Android Lint:Lint 检查、Baseline、问题级别和 CI 质量门禁。
- Test from the Command Line:本地单元测试、Instrumentation 测试和命令行测试任务。
- App Bundle:Android App Bundle、模块化交付和商店分发模型。
- Publish Your Library:Android Library 发布、AAR 构件和消费方配置。
- Configure Publication Variants:配置 Android Library 的可发布变体和构件。
37.35 自定义 Task、插件与构建逻辑
- Custom Tasks:自定义 Task、任务类型、输入输出、增量执行和缓存。
- Implementing Custom Tasks:使用类型安全属性实现可复用任务类型。
- Lazy Configuration:
Provider、Property、任务注册和延迟配置。 - Worker API:并行工作单元、隔离模式和外部工具执行。
- Build Services:共享资源、生命周期、并发限制和构建服务。
- Incremental Build:任务输入输出、增量执行和 up-to-date 检查。
- Build Cache:可缓存任务、缓存键、任务输出和远程缓存。
- Implementing Gradle Plugins:插件、扩展、任务注册和插件应用。
- Precompiled Script Plugins:使用 Kotlin DSL 创建预编译脚本插件。
- Sharing Build Logic Between Subprojects:使用
buildSrc、Included Build 和 Convention Plugin 共享构建逻辑。 - Testing Plugins:插件单元测试、功能测试和插件验证。
- Gradle TestKit:使用真实 Gradle Runner 测试自定义插件和构建逻辑。
- Validating Plugins:使用
validatePlugins检查插件 API、任务属性和兼容性。 - Publishing Gradle Plugins:Plugin Marker、Plugin Portal 和插件发布流程。
- Developing Binary Plugins:使用
java-gradle-plugin创建、测试和发布二进制插件。
37.36 完整项目、初始化与演进
- Build Init Plugin:使用
gradle init创建 Java、Kotlin、Groovy、Scala 和基础 Gradle 项目。 - Application Plugin:配置主类、
run、installDist、distZip和distTar。 - Java Plugin:Java Source Set、编译、JAR 和 Java 项目生命周期。
- Java Testing:JUnit、测试任务、测试过滤、测试日志和测试报告。
- Project Basics:项目、Settings、构建文件和 Gradle 项目模型基础。
- Settings File Basics:
settings.gradle.kts、根项目、子项目和构建边界。 - Build Lifecycle:初始化、配置、执行阶段和任务图。
- Multi-Project Builds:根项目、子项目、项目依赖和多项目任务。
- Sharing Build Logic Between Subprojects:把最小项目逐步演进为可维护的共享构建逻辑。
- Version Catalogs:使用
libs.versions.toml管理依赖版本和别名。 - Dependency Locking:为完整项目固定依赖解析结果并审查锁文件。
- Dependency Verification:校验和、PGP 和依赖来源验证。
- Maven Publish Plugin:为类库模块创建 Publication、POM 和发布任务。
- Publishing Setup:本地仓库演练、发布任务和消费方验证。
- Gradle Wrapper:固定项目 Gradle 版本并提交 Wrapper 文件。
- Command-Line Interface:执行任务、查看帮助、日志和构建扫描。
- Build Cache:在最小项目中启用并验证任务输出缓存。
- Configuration Cache:验证完整项目配置阶段缓存和插件兼容性。
- Build and Test Java with Gradle:使用 GitHub Actions 构建、测试、缓存和上传 Gradle 构件。
37.37 命令行、Wrapper 与脚本化执行
- Command-Line Interface:任务执行、命令行选项、日志级别、堆栈和构建扫描。
- Command-Line Interface Basics:Gradle 命令结构、任务路径、项目路径和基础选项。
- Gradle Wrapper:生成、升级、验证 Wrapper 和选择发行包类型。
- Gradle Wrapper Configuration:配置发行 URL、发行类型、校验和和 Wrapper 属性。
- Logging:
--quiet、--info、--debug、--stacktrace和日志输出。 - Build Environment:
gradle.properties、项目属性、系统属性、环境变量和 JVM 参数。 - Gradle Daemon:Daemon 状态、停止、JVM 参数和持续构建进程。
- Task Types:内置任务类型、任务选项、任务路径和任务行为。
- Build Lifecycle:初始化、配置、执行阶段和任务依赖图。
- Viewing and Debugging Dependencies:
dependencies、dependencyInsight和依赖解析结果。 - Dependency Locking:
--write-locks、--update-locks和锁文件管理。 - Dependency Verification:
--write-verification-metadata、严格验证和构件完整性。 - Continuous Build:
--continuous监视文件变化并重复执行任务。 - Build Cache:
--build-cache、--no-build-cache、缓存命中和任务输入输出。 - Configuration Cache:
--configuration-cache、--no-configuration-cache和配置阶段缓存。 - Build Scans:使用
--scan分析环境、任务、依赖、缓存和性能。 - Performance:使用
--profile、Build Scan 和任务数据分析构建性能。 - Publishing:
publish、publishToMavenLocal、Publication 和发布仓库任务。 - Testing in Java & JVM Projects:
test、--tests、测试过滤、报告和测试日志。 - Build and Test from the Command Line:Android 项目的构建、测试、安装和变体任务。
- Troubleshooting:根据任务、依赖、环境和日志分层诊断构建错误。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时