第 4 章:Gradle 专家级实践

本章目标

专家级 Gradle 能力不是会写更多语法,而是能让大型工程构建"快、稳、可治理、可追踪、可演进"。

本章覆盖:

  • 构建性能诊断与 Build Scan 深度使用
  • 依赖治理与依赖锁定
  • 依赖校验(Dependency Verification)
  • 代码质量接入(Checkstyle / SpotBugs)
  • 测试覆盖率(JaCoCo)
  • 自定义 Gradle 插件开发(独立插件)
  • Gradle TestKit(测试插件)
  • 制品发布与私服
  • Android/大型多模块工程治理
  • 疑难问题排查

1. 构建性能分析

Profile 报告

bash 复制代码
gradle clean build --profile

生成在 build/reports/profile/,是本地 HTML 报告。

Build Scan(推荐)

bash 复制代码
gradle clean build --scan

第一次会提示接受协议,之后生成在线报告链接(gradle.com)。

Build Scan 能看到:

  • 每个 task 耗时明细。
  • 哪些 task 是 FROM-CACHE / UP-TO-DATE / EXECUTED。
  • 依赖解析明细。
  • 测试结果。
  • 构建时的环境信息。

关注指标:

指标 说明 优化方向
Configuration time 配置阶段耗时 懒配置、减少根脚本逻辑、配置缓存
Task execution time task 执行耗时 增量构建、缓存、自定义 task 输入输出
Dependency resolution 依赖解析耗时 固定版本、减少动态版本、仓库治理
Avoidable work 可避免工作比例 缓存命中率优化

Gradle Enterprise / Develocity

企业级版本,支持:

  • 构建历史对比。
  • 团队级缓存(Build Cache Node)。
  • 构建性能趋势看板。
  • 失败根因分析。

接入(需要服务端):

groovy 复制代码
// settings.gradle
plugins {
    id 'com.gradle.enterprise' version '3.17'
}

gradleEnterprise {
    buildScan {
        server = 'https://ge.your-company.com'
        publishAlways()
    }
}

2. 大型工程优化顺序

不要一上来就改复杂插件,建议按顺序治理:

  1. 固定 JDK、Gradle、插件版本(Wrapper + Toolchain)。
  2. 开启并行构建和构建缓存。
  3. 清理动态版本依赖(禁止 1.+latest.release)。
  4. 抽取 Convention Plugin,减少子模块重复配置。
  5. 检查配置阶段耗时(--profile)。
  6. 验证配置缓存兼容性(--configuration-cache)。
  7. 对慢 task 做输入输出声明和增量改造。
  8. 接入 Build Scan 持续监控。

gradle.properties 推荐配置:

properties 复制代码
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.warning.mode=all

3. 依赖治理

常用命令

bash 复制代码
gradle :app:dependencies --configuration runtimeClasspath
gradle :app:dependencyInsight --dependency log4j --configuration runtimeClasspath
gradle dependencies --write-locks       # 生成锁文件
gradle dependencies --update-locks      # 更新锁文件中的特定依赖

依赖锁定

groovy 复制代码
dependencyLocking {
    lockAllConfigurations()
}

生成锁文件:

bash 复制代码
gradle dependencies --write-locks

锁文件(gradle/dependency-locks/runtimeClasspath.lockfile):

text 复制代码
# This is a Gradle generated file for dependency locking.
com.google.guava:guava:33.2.1-jre=runtimeClasspath
org.junit.jupiter:junit-jupiter:5.10.2=testRuntimeClasspath

适用场景:

  • 金融、支付、政企等高稳定性项目。
  • 对供应链安全要求高的项目。
  • 希望 CI 和本地依赖解析完全一致的项目。

治理策略

问题 处理方式
版本散落 Version Catalog
传递依赖冲突 dependencyInsight 定位来源
老版本安全风险 constraintsplatform 统一
动态版本不可重复 禁止 1.+latest.release
仓库来源混乱 settings.gradle 统一声明

4. 依赖校验(Dependency Verification)

Dependency Verification 通过校验文件的 SHA 哈希防止供应链攻击。

生成校验文件:

bash 复制代码
gradle --write-verification-metadata sha256 help

生成 gradle/verification-metadata.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<verification-metadata>
   <configuration>
      <verify-metadata>true</verify-metadata>
      <verify-signatures>false</verify-signatures>
   </configuration>
   <components>
      <component group="com.google.guava" name="guava" version="33.2.1-jre">
         <artifact name="guava-33.2.1-jre.jar">
            <sha256 value="a4d5...hash..." origin="Generated by Gradle"/>
         </artifact>
      </component>
   </components>
</verification-metadata>

之后每次构建都会校验 jar 的哈希,不匹配则构建失败。


5. 代码质量接入

Checkstyle

groovy 复制代码
plugins {
    id 'checkstyle'
}

checkstyle {
    toolVersion = '10.17.0'
    configFile = file("${rootDir}/config/checkstyle/checkstyle.xml")
    maxWarnings = 0
}

tasks.withType(Checkstyle).configureEach {
    reports {
        xml.required = true
        html.required = true
    }
}

config/checkstyle/checkstyle.xml 示例:

xml 复制代码
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
    "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
    "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
    <module name="TreeWalker">
        <module name="WhitespaceAround"/>
        <module name="NeedBraces"/>
        <module name="EqualsHashCode"/>
        <module name="MagicNumber"/>
    </module>
</module>

执行:

bash 复制代码
gradle checkstyleMain     # 检查主代码
gradle checkstyleTest     # 检查测试代码
gradle check              # 包含所有检查

SpotBugs

groovy 复制代码
plugins {
    id 'com.github.spotbugs' version '6.0.18'
}

spotbugs {
    toolVersion = '4.8.6'
    effort = 'max'
    reportLevel = 'medium'
    excludeFilter = file("${rootDir}/config/spotbugs/exclude.xml")
}

tasks.withType(com.github.spotbugs.snom.SpotBugsTask).configureEach {
    reports {
        html.required = true
        xml.required = false
    }
}

在 Convention Plugin 中统一接入

com.example.quality-conventions.gradle

groovy 复制代码
plugins {
    id 'checkstyle'
    id 'jacoco'
}

checkstyle {
    toolVersion = '10.17.0'
    configFile = rootProject.file('config/checkstyle/checkstyle.xml')
    maxWarnings = 0
}

jacoco {
    toolVersion = '0.8.12'
}

tasks.named('test') {
    finalizedBy tasks.named('jacocoTestReport')
}

tasks.named('jacocoTestReport') {
    dependsOn tasks.named('test')
    reports {
        xml.required = true
        html.required = true
    }
}

6. 测试覆盖率(JaCoCo)

groovy 复制代码
plugins {
    id 'java'
    id 'jacoco'
}

jacoco {
    toolVersion = '0.8.12'
}

test {
    useJUnitPlatform()
    finalizedBy jacocoTestReport
}

jacocoTestReport {
    dependsOn test
    reports {
        xml.required = true
        html.required = true
        csv.required = false
    }
}

// 设置覆盖率门禁
jacocoTestCoverageVerification {
    violationRules {
        rule {
            limit {
                minimum = 0.80   // 行覆盖率不低于 80%
            }
        }
    }
}

check.dependsOn jacocoTestCoverageVerification

执行:

bash 复制代码
gradle test jacocoTestReport          # 执行测试并生成报告
gradle jacocoTestCoverageVerification # 验证覆盖率是否达标

报告位置:build/reports/jacoco/test/html/index.html

多模块汇总覆盖率:

groovy 复制代码
// 根模块 build.gradle
tasks.register('jacocoRootReport', JacocoReport) {
    dependsOn subprojects.collect { it.tasks.named('test') }
    sourceDirectories.from subprojects.collect { it.sourceSets.main.allSource.srcDirs }
    classDirectories.from subprojects.collect { it.sourceSets.main.output }
    executionData.from subprojects.collect { it.tasks.named('test').get().extensions.getByType(JacocoTaskExtension).destinationFile }
    reports {
        html.required = true
        xml.required = true
    }
}

7. 自定义 Gradle 插件(独立发布)

独立插件项目结构:

text 复制代码
my-gradle-plugin/
├── settings.gradle
├── build.gradle
└── src/main/groovy/
    └── com/example/MyPlugin.groovy

build.gradle

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

group = 'com.example'
version = '1.0.0'

gradlePlugin {
    plugins {
        myPlugin {
            id = 'com.example.my-plugin'
            implementationClass = 'com.example.MyPlugin'
        }
    }
}

插件实现:

groovy 复制代码
package com.example

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        def extension = project.extensions.create('myConfig', MyExtension)

        project.tasks.register('myTask') {
            group = 'my-group'
            description = 'A custom task from my plugin'
            doLast {
                println "Running with config: ${extension.message.getOrElse('default')}"
            }
        }
    }
}

Extension:

groovy 复制代码
package com.example

import org.gradle.api.provider.Property

abstract class MyExtension {
    abstract Property<String> getMessage()
}

发布到本地:

bash 复制代码
gradle publishToMavenLocal

其他项目使用:

groovy 复制代码
// settings.gradle
pluginManagement {
    repositories {
        mavenLocal()
        gradlePluginPortal()
    }
}

// build.gradle
plugins {
    id 'com.example.my-plugin' version '1.0.0'
}

myConfig {
    message = 'Hello from custom plugin!'
}

8. Gradle TestKit(测试自定义插件)

TestKit 允许用 JUnit 测试 Gradle 插件的行为:

groovy 复制代码
dependencies {
    testImplementation gradleTestKit()
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}

test {
    useJUnitPlatform()
}

测试插件:

java 复制代码
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.BuildResult;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

import static org.gradle.testkit.runner.TaskOutcome.SUCCESS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MyPluginTest {

    @TempDir
    File testProjectDir;

    @Test
    void myTaskShouldSucceed() throws IOException {
        // 创建临时项目
        Files.writeString(testProjectDir.toPath().resolve("settings.gradle"),
            "rootProject.name = 'test-project'");
        Files.writeString(testProjectDir.toPath().resolve("build.gradle"), """
            plugins {
                id 'com.example.my-plugin'
            }
            myConfig {
                message = 'TestKit works!'
            }
        """);

        // 执行 task
        BuildResult result = GradleRunner.create()
            .withProjectDir(testProjectDir)
            .withArguments("myTask")
            .withPluginClasspath()   // 自动加载当前插件
            .build();

        // 断言
        assertEquals(SUCCESS, result.task(":myTask").getOutcome());
        assertTrue(result.getOutput().contains("TestKit works!"));
    }
}

9. 制品发布

发布到本地 Maven 仓库

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

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
            // 补充 POM 元数据
            pom {
                name = 'My Library'
                description = 'A concise description of my library'
                url = 'https://github.com/example/my-library'
                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')
        }
        mavenLocal()
    }
}
bash 复制代码
gradle publish                  # 发布到所有配置的仓库
gradle publishToMavenLocal      # 只发布到本机 ~/.m2
gradle :common:publish          # 只发布 common 模块

发布到私服(Nexus / Artifactory)

groovy 复制代码
publishing {
    repositories {
        maven {
            name = 'nexus'
            def isSnapshot = version.toString().endsWith('-SNAPSHOT')
            url = isSnapshot
                ? uri('https://nexus.company.com/repository/maven-snapshots/')
                : uri('https://nexus.company.com/repository/maven-releases/')
            credentials {
                username = System.getenv('NEXUS_USERNAME') ?: ''
                password = System.getenv('NEXUS_PASSWORD') ?: ''
            }
        }
    }
}

安全注意

  • 不要把账号密码写进 build.gradle
  • 使用环境变量或 CI Secret。
  • 区分 snapshot 和 release 仓库。

发布到 Maven Central(Sonatype)

需要额外步骤:

  1. 注册 Sonatype 账号,申请 groupId
  2. 配置 GPG 签名。
  3. 使用 signing 插件。
groovy 复制代码
plugins {
    id 'signing'
    id 'maven-publish'
}

signing {
    sign publishing.publications.mavenJava
}
bash 复制代码
gradle publishMavenJavaPublicationToSonatypeRepository \
    -Psigning.keyId=ABCDEF12 \
    -Psigning.password=secret \
    -Psigning.secretKeyRingFile=~/.gnupg/secring.gpg

10. Android 大型项目治理

Android Gradle 工程常见痛点:

问题 方向
模块配置重复 Convention Plugin
编译慢(KAPT) 迁移到 KSP
Variant 太多 控制 flavor 维度,减少无效变体
依赖冲突 Version Catalog + dependencyInsight
CI 慢 分层流水线、缓存 Gradle User Home
R8/ProGuard 问题 建立 release 构建验证流程

KSP 替代 KAPT(显著提升编译速度):

groovy 复制代码
// 原 KAPT
plugins { id 'kotlin-kapt' }
dependencies { kapt 'com.google.dagger:dagger-compiler:2.51' }

// 改为 KSP
plugins { id 'com.google.devtools.ksp' version '2.0.0-1.0.24' }
dependencies { ksp 'com.google.dagger:dagger-compiler:2.51' }

版本兼容矩阵(升级前必须确认):

组件 当前版本 说明
Gradle 8.x 见 AGP 兼容表
Android Gradle Plugin 8.x 和 Gradle 版本强绑定
Kotlin 2.x 和 KSP 版本对齐
JDK 17 或 21 AGP 8.x 要求 JDK 17+

11. 故障排查方法

依赖无法下载

bash 复制代码
gradle build --refresh-dependencies --stacktrace --info

排查:

  • 仓库地址是否正确。
  • 依赖坐标是否存在(去 Maven Central 搜索确认)。
  • 网络代理是否生效。
  • 是否被公司私服策略拦截。

插件找不到

检查 pluginManagement.repositories

groovy 复制代码
pluginManagement {
    repositories {
        gradlePluginPortal()
        mavenCentral()
        google()          // Android 项目需要
        mavenLocal()      // 本地开发插件
    }
}

构建突然变慢

bash 复制代码
gradle build --profile
gradle build --scan
gradle help --configuration-cache

重点看:

  • 是否新增了配置阶段 IO。
  • 是否新增了动态版本依赖。
  • 是否缓存失效(--info 看 UP-TO-DATE 判断)。
  • 是否 task 输入输出声明缺失。

本地成功,CI 失败

排查步骤:

bash 复制代码
gradle --version            # 确认 Gradle 版本
java -version               # 确认 JDK 版本
git status                  # 确认没有未提交文件
gradle clean build --stacktrace

重点检查:

  • JDK 版本(本地 vs CI)。
  • Gradle Wrapper(是否提交)。
  • 环境变量(CI 缺少本地设置的变量)。
  • 私服凭证(CI 未配置)。
  • 文件大小写差异(Linux CI 区分大小写)。

OutOfMemoryError

properties 复制代码
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError

Android 项目额外在模块 gradle.properties

properties 复制代码
android.enableJetifier=false   # 如果没有旧版依赖,关闭 Jetifier

12. 专家级检查清单

检查项 合格标准
Gradle 版本 使用 Wrapper 固定
JDK 版本 通过 Toolchain 声明,本地和 CI 一致
仓库声明 settings.gradle 统一
依赖版本 使用 Version Catalog 或平台管理
动态版本 禁止(无 1.+latest.release
子模块配置 通过 Convention Plugin 统一
构建缓存 开启并验证有效
配置缓存 定期验证兼容性
代码质量 Checkstyle / SpotBugs 接入 check 生命周期
测试覆盖率 JaCoCo 接入,有门禁阈值
发布流程 测试通过后发布,凭证不入库
故障定位 能用 dependenciesdependencyInsight--scan--stacktrace

13. 实操:发布 demo 的 common 模块

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

# 发布到本地构建目录
gradle :common:publish

# 验证产物
find common/build/repo -type f

应该能看到:

text 复制代码
common/build/repo/com/example/gradle-demo-common/1.0.0/
├── gradle-demo-common-1.0.0.jar
├── gradle-demo-common-1.0.0-sources.jar
├── gradle-demo-common-1.0.0.module
└── gradle-demo-common-1.0.0.pom

查看 JaCoCo 覆盖率报告:

bash 复制代码
gradle :common:test :common:jacocoTestReport
open common/build/reports/jacoco/test/html/index.html

14. 面试和工程表达

问:Gradle 为什么比传统构建工具更适合大型项目

完整回答

Gradle 围绕大型工程提供了完整能力:多模块构建、插件扩展、增量构建、构建缓存、配置缓存、依赖解析、任务图、发布体系和 CI 集成。大型项目的核心问题是可维护性性能。Gradle 可以通过 Convention Plugin 统一规范,通过 Version Catalog 治理依赖,通过缓存和懒配置优化性能,通过 Toolchain 和 Wrapper 保证可重复性。

追问点:

  • 如何避免 Gradle 脚本越来越乱。
  • 如何定位构建慢。
  • 如何统一多模块依赖版本。

问:你如何治理一个构建很慢的 Gradle 项目

完整回答

先用 --profile 或 Build Scan 获取耗时事实,区分是配置阶段慢、依赖解析慢,还是 task 执行慢。然后按顺序处理:固定版本和 Wrapper,开启并行构建和构建缓存,清理动态依赖,减少子模块重复配置,抽取 Convention Plugin,验证配置缓存,对自定义 task 补充输入输出声明。不会一开始凭感觉重构构建脚本。

问:apiimplementation 怎么选

完整回答

如果依赖类型出现在模块对外公开的 API 中(public 方法参数、返回值、父类、接口),就用 api。如果依赖只在模块内部实现中使用,就用 implementation。默认优先使用 implementation,因为它能减少下游编译 classpath,降低模块耦合,提高增量编译效率。

问:如何保证构建的可重复性

完整回答

可重复性需要在多个层面锁定:

  1. Gradle 版本:通过 Wrapper 固定。
  2. JDK 版本:通过 Toolchain 声明。
  3. 依赖版本:通过 Version Catalog 集中管理,禁止动态版本,必要时开启 Dependency Locking。
  4. 依赖来源 :在 settings.gradle 统一仓库,企业内通过私服代理外部仓库。
  5. 构建环境 :CI 使用固定镜像,用 --no-daemon 避免 Daemon 状态污染。
相关推荐
黄林晴1 天前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
followYouself7 天前
Gradle、AGP、Plugin插件基本知识
android·gradle·plugin·agp
千码君201610 天前
Flutter:在win10上第一次安装和尝试开发记录
flutter·gradle·android-studio·安卓模拟器
Ww.xh19 天前
Flutter配置Gradle完整教程
flutter·gradle·android studio
vortex520 天前
Gradle 从入门到实战
java·gradle
蜡台23 天前
Android Studio Gradlew JDK配置
java·gradle·android studio·intellij-idea
spencer_tseng25 天前
java.net.SocketTimeoutException: Connect timed out
gradle
蜡台1 个月前
Android Studio 高版本兼容低版本项目配置
android·ide·jdk·gradle·android studio
狂龙骄子1 个月前
Android Studio下载与版本选择指南
jdk·gradle·android studio·intellij idea·androidsdk·agp·归档版本