11、自定义Task与Plugin:Gradle Task创建方式(doLast/doFirst/type)、自定义Plugin开发(buildSrc/独立工程)、Extension扩展配置

说实话,Gradle 构建脚本写多了,你会发现重复的 Task 逻辑越来越多。这时候就该考虑自定义 Task 和 Plugin 了。我刚开始接触 Gradle 时,总觉得「写个 Task 不就行了?搞什么 Plugin 啊」。直到项目越来越大,十几个模块都要做同样的签名、混淆、渠道包操作……嗯,那时候我才意识到,没有抽象和复用,维护成本会高到让你想砸电脑。

这一章,我们就来聊聊怎么优雅地创建 Task,怎么开发自己的 Plugin,以及怎么用 Extension 让配置变得灵活。说白了,就是让你从「脚本仔」进化成「构建架构师」。

11.1 Task 的三种创建方式

Gradle 里创建 Task 的方式很多,但最常用的就三种。我个人习惯把它们分成「轻量级」「标准级」「定制级」。

11.1.1 直接用 doLast / doFirst

这是最直接的方式。适合临时调试、快速验证想法。你不需要定义 Task 类型,直接写闭包就行。

tasks.register('helloTask') {
    doLast {
        println 'Hello, Gradle!'
    }
    doFirst {
        println '我先执行'
    }
}

执行顺序:doFirst 先跑,doLast 后跑。我在项目中经常用 doLast 来打印构建耗时、输出 APK 路径等信息。比如:

tasks.whenTaskAdded { task ->
    task.doLast {
        println "${task.name} 执行完毕,耗时: ${System.currentTimeMillis()}"
    }
}
小技巧:doFirst 和 doLast 可以叠加多次。你可以在不同地方给同一个 Task 追加行为,Gradle 会按顺序执行。

11.1.2 通过 type 指定 Task 类型

Gradle 内置了很多 Task 类型,比如 CopyDeleteExecZip 等。你只需要指定 type,然后配置属性即可。这比手写 doLast 要安全得多。

tasks.register('backupApk', Copy) {
    from layout.buildDirectory.dir('outputs/apk/debug')
    into layout.buildDirectory.dir('backup')
    include '*.apk'
}

我曾经犯过一个错误:用 doLast 写文件拷贝,结果每次构建都报「文件被占用」。后来换成 Copy 类型,Gradle 自动处理了输入输出依赖,问题就解决了。你想想看,内置类型帮你处理了增量构建、文件锁定这些脏活,何必自己造轮子?

内置 Task 类型 用途 常用属性
Copy 文件拷贝 from, into, include, exclude
Delete 删除文件/目录 delete
Exec 执行系统命令 commandLine, workingDir
Zip 打包成 zip from, archiveName, destinationDir
JavaExec 运行 Java 程序 main, classpath, args

11.1.3 自定义 Task 类

当逻辑复杂到一定程度,比如需要多个输入输出、需要支持增量构建,那就得写一个自定义 Task 类了。我建议放在 buildSrc 里,或者独立工程中。

abstract class MyTask extends DefaultTask {

    @Input
    abstract Property<String> getMessage()

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void doAction() {
        outputFile.get().asFile.writeText(message.get())
        logger.lifecycle("写入完成: ${message.get()}")
    }
}

tasks.register('myTask', MyTask) {
    message = 'Hello from custom task'
    outputFile = layout.buildDirectory.file('output.txt')
}
注意:自定义 Task 类一定要用 abstract 关键字声明属性,Gradle 会帮你生成实现。别问我为什么,这是 Gradle 7.0+ 的推荐做法。我曾经用普通属性踩过坑,结果增量构建一直不生效。

11.2 自定义 Plugin 开发

Task 写多了,你会发现很多 Task 是成组出现的。比如「打渠道包」需要先清理、再编译、再签名、再对齐。这时候 Plugin 就派上用场了。Plugin 可以把一组 Task 和配置打包成一个可复用的单元。

11.2.1 用 buildSrc 实现

这是最轻量的方式。在项目根目录创建 buildSrc 目录,里面放一个 build.gradle.kts 和源码。所有模块都能自动引用。

// buildSrc/build.gradle.kts
plugins {
    `java-gradle-plugin`
}

// buildSrc/src/main/java/com/example/MyPlugin.java
public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        project.getTasks().register("myPluginTask", task -> {
            task.doLast(s -> System.out.println("来自 Plugin 的问候"));
        });
    }
}

然后在 app 模块直接执行 ./gradlew myPluginTask 就能看到效果。我个人习惯把公共的构建逻辑放在 buildSrc 里,比如签名配置、版本号管理。这样所有模块都能共享,而且编译速度比独立工程快。

避坑指南:buildSrc 的改动会导致整个项目重新构建。我曾经为了改一个日志输出,等了 3 分钟全量编译。所以如果 Plugin 逻辑很复杂,建议用独立工程。

11.2.2 独立工程发布

当 Plugin 需要跨项目复用,或者需要发布到内部仓库时,独立工程是更好的选择。结构大概是这样的:

my-plugin/
├── build.gradle.kts
├── src/main/
│   ├── java/
│   │   └── com/example/MyPlugin.java
│   └── resources/
│       └── META-INF/gradle-plugins/
│           └── com.example.myplugin.properties

properties 文件内容:

implementation-class=com.example.MyPlugin

发布到 maven 仓库后,其他项目只需要:

plugins {
    id("com.example.myplugin") version "1.0.0"
}

我记得有一次,公司内部多个项目都需要做「资源混淆」功能。我把它做成独立 Plugin 发布到私有 Nexus 上,每个项目只需一行配置。嗯,那种感觉确实很爽。

11.3 Extension 扩展配置

Plugin 写好了,但用户怎么传参数?比如渠道名、版本号、是否开启混淆。这时候就需要 Extension 了。Extension 说白了就是给 Plugin 提供一个配置块,让用户在 build.gradle 里写 DSL。

// 定义 Extension 类
abstract class MyExtension {
    abstract Property<String> getChannel()
    abstract Property<Boolean> getEnableObfuscation()
}

// 在 Plugin 中注册
public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        MyExtension extension = project.getExtensions()
            .create("myConfig", MyExtension.class);

        project.getTasks().register("printConfig", task -> {
            task.doLast(s -> {
                System.out.println("渠道: " + extension.getChannel().get());
                System.out.println("混淆: " + extension.getEnableObfuscation().get());
            });
        });
    }
}

用户在 build.gradle 里这样用:

myConfig {
    channel = "huawei"
    enableObfuscation = true
}
核心要点:Extension 的命名就是 DSL 块的名字。你可以嵌套 Extension,实现类似 android { defaultConfig { ... } } 的多层结构。我建议保持扁平化,嵌套太深会让用户抓狂。

11.4 知识体系总览

下面这张图概括了本章的核心脉络。你可以看到 Task 是基础,Plugin 是组织单元,Extension 是配置入口。三者配合,才能写出优雅的构建逻辑。

自定义 Task 与 Plugin 知识体系 Task 创建方式 Plugin 开发 Extension 配置 doLast / doFirst type 指定内置类型 自定义 Task 类 buildSrc 方式 独立工程发布 META-INF 注册 Extension 类定义 getExtensions().create() DSL 配置块 Task 是执行单元,Plugin 是组织单元,Extension 是配置入口

11.5 避坑与最佳实践

  • 不要滥用 doLast:我曾经在一个项目里看到 200 多行 doLast 嵌套,维护起来简直噩梦。能用 type 就用 type,能抽象成 Task 类就抽象。
  • Plugin 的 apply 方法不要写太重逻辑:它只负责注册 Task 和 Extension。真正的执行逻辑放在 Task 的 @TaskAction 里。
  • Extension 属性尽量用 Property 类型:这样 Gradle 可以帮你做延迟计算和配置避免。用普通 String 的话,配置顺序一乱就出 bug。
  • 独立 Plugin 记得加 group 和 version:不然发布到仓库后,别人根本找不到你的 artifact。
我曾经踩过的坑:在 Plugin 的 apply 方法里直接读取了 Extension 的值,结果用户还没配置完,Extension 返回 null。正确的做法是在 TaskAction 里读取,或者用 project.afterEvaluate 包裹。

好了,这一章的内容就到这里。Task、Plugin、Extension 这三板斧用好了,你的构建脚本会变得既清晰又强大。下次再遇到重复的构建逻辑,别犹豫,直接上 Plugin。


公众号:蓝海资料掘金营,微信deep3321