手把手带你自定义 Gradle 插件 —— Gradle 系列(2)

2023-07-24

请点赞加关注,你的支持对我非常重要,满足下我的虚荣心。

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,欢迎跟着我一起成长。(联系方式在 GitHub)

前言

Gradle 本质上是高度模块化的构建逻辑,便于重用并与他人分享。例如,我们熟悉的 Android 构建流程就是由 Android Gradle Plugin 引入的构建逻辑。在这篇文章里,我将带你探讨 Gradle 插件的使用方法、开发步骤和技巧总结。

这篇文章是全面掌握 Gradle 构建系统系列的第 2 篇:

1、Gradle 基础
2、Gradle 插件
3、Gradle 依赖管理
4、APG Transform


1. 认识 Gradle 插件

1.1 什么是 Gradle 插件

Gradle 和 Gradle 插件是两个完全不同的概念,Gradle 提供的是一套核心的构建机制,而 Gradle 插件则是运行在这套机制上的一些具体构建逻辑,本质上和 .gradle 文件是相同。例如,我们熟悉的编译 Java 代码的能力,都是由插件提供的。

1.2 Gradle 插件的优点

虽然 Gradle 插件与 .gradle 文件本质上没有区别,.gradle 文件也能实现 Gradle 插件类似的功能。但是,Gradle 插件使用了独立模块封装构建逻辑,无论是从开发开始使用来看,Gradle 插件的整体体验都更友好。

逻辑复用: 将相同的逻辑提供给多个相似项目复用,减少重复维护类似逻辑开销。当然 .gradle 文件也能做到逻辑复用,但 Gradle 插件的封装性更好;
组件发布: 可以将插件发布到 Maven 仓库进行管理,其他项目可以使用插件 ID 依赖。当然 .gradle 文件也可以放到一个远程路径被其他项目引用;
构建配置: Gradle 插件可以声明插件扩展来暴露可配置的属性,提供定制化能力。当然 .gradle 文件也可以做到,但实现会麻烦些。

1.3 插件的两种实现形式

Gradle 插件的核心类是 Plugin,一般使用 Project 作为泛型实参。当使用方引入插件后,其实就是调用了 Plugin#apply() 方法,我们可以把 apply() 方法理解为插件的执行入口。例如:

MyCustomGradlePlugin.groovy

public class MyCustomGradlePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println "Hello."
}
}

如果根据实现形式分类(MyCustomGradlePlugin 的代码位置),可以把 Gradle 插件分为 2 类:

1、脚本插件: 脚本插件就是一个普通的脚本文件,它可以被导入都其他构建脚本中。有的朋友说脚本插件也需要使用 Plugin 接口才算脚本插件,例如:

build.gradle

apply plugin: MyCustomGradlePlugin

class MyCustomGradlePlugin implements Plugin<Project> {
...
}

2、二进制插件 / 对象插件: 在一个单独的插件模块中定义,其他模块通过 Plugin ID 应用插件。因为这种方式发布和复用更加友好,我们一般接触到的 Gradle 插件都是指二进制插件的形式。

1.4 应用插件的步骤

我们总结下使用二进制插件的步骤:

1、将插件添加到 classpath: 将插件添加到构建脚本的 classpath 中,我们的 Gradle 构建脚本才能应用插件。这里区分本地依赖和远程依赖两种情况。

本地依赖: 指直接依赖本地插件源码,一般在调试插件的阶段是使用本地依赖的方式。例如:

项目 build.gradle

buildscript {
...
dependencies {
// For Debug
classpath project(":easyupload")
}
}

远程依赖: 指依赖已发布到 Maven 仓库的插件,一般我们都是用这种方式依赖官方或第三方实现的 Gradle 插件。例如:

项目 build.gradle

buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
// 也可以使用另一种等价语法:
classpath group: 'com.android.tools.build ', name: 'gradle ', version: '3.5.3'
}
...
}

2、使用 apply 应用插件: 在需要使用插件的 .gradle 脚本中使用 apply 应用插件,这将创建一个新的 Plugin 实例,并执行 Plugin#apply() 方法。例如:

apply plugin: 'com.android.application'

// 或者

plugins {
// id «plugin id» [version «plugin version»] [apply «false»]
id 'com.android.application'
}

注意: 不支持在一个 build.gradle 中同时使用这两种语法。

1.5 特殊的 buildSrc 模块

插件模块的名称是任意的,除非使用了一个特殊的名称 “buildSrc”,buildSrc 模块是 Gradle 默认的插件模块。buildSrc 模块本质上和普通的插件模块是一样的,有一些小区别:

1、buildSrc 模块会被自动识别为参与构建的模块,因此不需要在 settings.gradle 中使用 include 引入,就算引入了也会编译出错:

Build OutPut:
'buildSrc' cannot be used as a project name as it is a reserved name

2、buildSrc 模块会自动被添加到构建脚本的 classpath 中,不需要手动添加:

buildscript {
...
dependencies {
// 不需要手动添加
// classpath project(":buildSrc")
}
}

3、buildSrc 模块的 build.gradle 执行时机早于其他 Project:

Executing tasks: [test] 

settings.gradle:This is executed during the initialization phase.

> Configure project :buildSrc
build.gradle:buildSrc. > Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources NO-SOURCE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :buildSrc:assemble UP-TO-DATE
> Task :buildSrc:pluginUnderTestMetadata UP-TO-DATE
> Task :buildSrc:compileTestJava NO-SOURCE
> Task :buildSrc:compileTestGroovy NO-SOURCE
> Task :buildSrc:processTestResources NO-SOURCE
> Task :buildSrc:testClasses UP-TO-DATE
> Task :buildSrc:test NO-SOURCE
> Task :buildSrc:validatePlugins UP-TO-DATE
> Task :buildSrc:check UP-TO-DATE
> Task :buildSrc:build UP-TO-DATE
...
> Configure project :
...
> Task :test
...
BUILD SUCCESSFUL in 19s


2. 自定义 Gradle 插件的步骤

这一节我们来讲实现 Gradle 插件的具体步骤,基本步骤分为 5 步:

1、初始化插件目录结构
2、创建插件实现类
3、配置插件实现类
4、发布插件
5、使用插件

2.1 初始化插件目录结构

首先,我们在 Android Studio 新建一个 Java or Kotlin Library 模块,这里以非 buildSrc 模块的情况为例:

然后,将模块 build.gradle 文件替换为以下内容:

模块 build.gradle

plugins {
id 'groovy' // Groovy Language
id 'org.jetbrains.kotlin.jvm' // Kotlin
id 'java-gradle-plugin' // Java Gradle Plugin
}

groovy 插件: 使用 Groovy 语言开发必备;
org.jetbrains.kotlin.jvm 插件: 使用 Kotlin 语言开发必备;
java-gradle-plugin 插件: 用于帮助开发 Gradle 插件,会自动应用 Java Library 插件,并在 dependencies 中添加 implementation gradleApi()

最后,根据你需要的开发语言补充对应的源码文件夹,不同语言有默认的源码文件夹,你也可以在 build.gradle 文件中重新指定:

模块 build.gradle

plugins {
id 'groovy' // Groovy Language
id 'org.jetbrains.kotlin.jvm' // Kotlin
id 'java-gradle-plugin' // Java Gradle Plugin
} sourceSets {
main {
groovy {
srcDir 'src/main/groovy'
} java {
srcDir 'src/main/java'
} resources {
srcDir 'src/main/resources'
}
}
}

插件目录结构:

2.2 创建插件实现类

新建一个 Plugin 实现类,并重写 apply 方法中添加构建逻辑,例如:

com.pengxr.easyupload.EasyUpload.groovy

class EasyUpload implements Plugin<Project> {

    @Override
void apply(Project project) {
// 构建逻辑
println "Hello."
}
}

2.3 配置插件实现类

在模块 build.gradle 文件中增加以下配置,gradlePlugin 定义了插件 ID 和插件实现类的映射关系:

gradlePlugin {
plugins {
modularPlugin {
// Plugin id.
id = 'com.pengxr.easyupload'
// Plugin implementation.
implementationClass = 'com.pengxr.easyupload.EasyUpload'
}
}
}

这其实是 Java Gradle Plugin 提供的一个简化 API,其背后会自动帮我们创建一个 [插件ID].properties 配置文件,Gradle 就是通过这个文件类进行匹配的。如果你不使用 gradlePlugin API,直接手动创建 [插件ID].properties 文件,作用是完全一样的。

要点:

1、[插件ID].properties 文件名是插件 ID,用于应用插件
2、[插件ID].properties 文件内容配置了插件实现类的映射,需要使用implementation-class来指定插件实习类的全限定类名

implementation-class=com.pengxr.easyupload.EasyUpload

2.4 发布插件

我们使用 maven 插件 来发布仓库,在模块 build.gradle 文件中增加配置:

模块 build.gradle

plugins {
id 'groovy' // Groovy Language
id 'org.jetbrains.kotlin.jvm' // Kotlin
id 'java-gradle-plugin' // Java Gradle Plugin
} gradlePlugin {
plugins {
modularPlugin {
// Plugin id.
id = 'com.pengxr.easyupload'
// Plugin implementation.
implementationClass = 'com.pengxr.easyupload.EasyUpload'
}
}
} uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('../localMavenRepository/snapshot'))
pom.groupId = 'com.pengxr'
pom.artifactId = 'easyupload'
pom.version = '1.0.0'
}
}
}

执行 uploadArchives 任务,会发布插件到项目根目录中的 localMavenRepository 文件夹,实际项目中通常是发布到 Nexus 私库或 Github 公共库等。不熟悉组件发布的话可以回顾:Android工程化实践:组件化发布,此处不展开。

2.5 使用插件

在项目级 build.gradle 文件中将插件添加到 classpath:

项目 build.gradle

buildscript {
repositories {
google()
jcenter()
maven { url "$rootDir/localMavenRepository/snapshot" }
maven { url "$rootDir/localMavenRepository/release" }
}
dependencies {
// For debug
// classpath project(":easyupload")
classpath "com.pengxr:easyupload:1.0.0"
}
...
}

在模块级 build.gradle 文件中 apply 插件:

模块 build.gradle

// '项目 build.gradle' 是在 gradlePlugin 中定义的插件 ID
apply plugin: 'com.pengxr.easyupload'

完成以上步骤并同步项目,从 Build Output 可以看到我们的插件生效了:

Build Output:

Hello.

到这里,自定义 Gradle 插件最基本的步骤就完成了,接下来就可以在 Plugin#apply 方法中开始你的表演。


3. 插件扩展机制

Extension 扩展是插件为外部构建脚本提供的配置项,用于支持外部自定义插件的工作方式,其实就是一个对外开放的 Java Bean 或 Groovy Bean。例如,我们熟悉的 android{} 就是 Android Gradle Plugin 提供的扩展。

当你应用一个插件时,插件定义的扩展会以 扩展名-扩展对象 键值对的形式保存在 Project 中的 ExtensionContainer 容器中。插件内外部也是通过 ExtensionContainer 访问扩展对象的。

注意事项:

扩展名: 不支持在同一个 Project 上添加重复的扩展名;
映射关系: 添加扩展后,不支持重新设置扩展对象;
DSL: 支持用 扩展名 {} DSL 的形式访问扩展对象。

3.1 基本步骤

这一节我们来讲实现 Extension 扩展的具体步骤,基本步骤分为 5 步:

1、定义扩展类: 定义一个扩展配置类:

Upload.groovy

class Upload {
String name
}

提示: 根据 ”约定优先于配置“ 原则,尽量为配置提供默认值,或者保证配置缺省时也能正常执行。

2、创建并添加扩展对象: 在 Plugin#apply() 中,将扩展对象添加到 Project 的 ExtensionContainer 容器中:

EasyUpload.groovy

class EasyUpload implements Plugin<Project> {

    // 扩展名
public static final String UPLOAD_EXTENSION_NAME = "upload" @Override
void apply(Project project) {
// 添加扩展
applyExtension(project)
// 添加 Maven 发布能力
applyMavenFeature(project)
} private void applyExtension(Project project) {
// 创建扩展,并添加到 ExtensionContainer
project.extensions.create(UPLOAD_EXTENSION_NAME, Upload)
} private void applyMavenFeature(Project project) {
// 构建逻辑
}
}

3、配置扩展: 使用方应用插件后,使用 扩展名 {} DSL定制插件行为:

build.gradle

apply plugin: 'com.pengxr.easyupload'

upload {
name = "Peng"
}

4、使用扩展: 在 Plugin#apply() 中,通过 Project 的 ExtensionContainer 容器获取扩展对象,获取的代码建议封装在扩展对象内部。例如:

class EasyUpload implements Plugin<Project> {

    // 扩展名
public static final String UPLOAD_EXTENSION_NAME = "upload" @Override
void apply(Project project) {
// 添加扩展
applyExtension(project)
// 添加 Maven 发布能力
applyMavenFeature(project)
} private void applyExtension(Project project) {
// 创建扩展,并添加到 ExtensionContainer 容器
project.extensions.create(UPLOAD_EXTENSION_NAME, Upload)
} private void applyMavenFeature(Project project) {
project.afterEvaluate {
// 1. Upload extension
Upload rootConfig = Upload.getConfig(project.rootProject)
// 构建逻辑 ...
}
}
}

Upload.groovy

class Upload {

    String name

    // 将获取扩展对象的代码封装为静态方法
static Upload getConfig(Project project) {
// 从 ExtensionContainer 容器获取扩展对象
Upload extension = project.getExtensions().findByType(Upload.class)
// 配置缺省的时候,赋予默认值
if (null == extension) {
extension = new Upload()
}
return extension
} /**
* 检查扩展配置是否有效
*
* @return true:valid
*/
boolean checkParams() {
return true
}
}

提示: ExtensionContainer#create() 支持变长参数,支持调用扩展类带参数的构造函数,例如:project.extensions.create(UPLOAD_EXTENSION_NAME, Upload,"Name") 将调用构造函数 Upload(String str)

5、构建逻辑: 到这里,实现插件扩展最基本的步骤就完成了,接下来就可以在 Plugin#apply 方法中继续完成你的表演。

3.2 project.afterEvaluate 的作用

使用插件扩展一定会用到 project.afterEvaluate() 生命周期监听,这里解释一下:因为扩展配置代码的执行时机晚于 Plugin#apply() 的执行时机,所以如果不使用 project.afterEvaluate(),则在插件内部将无法正确获取配置值。

project.afterEvaluate() 会在当前 Project 配置完成后回调,这个时机扩展配置代码已经执行,在插件内部就可以正确获取配置值。

apply plugin: 'com.pengxr.easyupload'

// 执行时机晚于 apply
upload {
name = "Peng"
}

3.3 嵌套扩展

在扩展类中组合另一个配置类的情况,我们称为嵌套扩展,例如我们熟悉的 defaultConfig{} 就是一个嵌套扩展:

android {
compileSdkVersion 30
buildToolsVersion "30.0.0"
defaultConfig {
minSdkVersion 21
...
}
...
}

默认下嵌套扩展是不支持使用闭包配置,我们需要在外部扩展类中定义闭包函数。例如:

Upload.groovy

class Upload {

    // 嵌套扩展
Maven maven // 嵌套扩展
Pom pom // 嵌套扩展闭包函数,方法名为 maven(方法名不一定需要与属性名一致)
void maven(Action<Maven> action) {
action.execute(maven)
} // 嵌套扩展闭包函数,方法名为 maven
void maven(Closure closure) {
ConfigureUtil.configure(closure, maven)
} // 嵌套扩展闭包函数,方法名为 pom
void pom(Action<Pom> action) {
action.execute(pom)
} // 嵌套扩展闭包函数,方法名为 pom
void pom(Closure closure) {
ConfigureUtil.configure(closure, pom)
}
}

使用时:

build.gradle

apply plugin: 'com.pengxr.easyupload'

upload {
maven {
...
}
}

3.4 NamedDomainObjectContainer 命名 DSL

在 Android 工程中,你一定在 build.gradle 文件中见过以下配置:

build.gradle

android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
...
}
// 支持任意命名
preview {
...
}
}
}

除了内置的 release 和 debug,我们可以在 buildType 中定义任意多个且任意名称的类型,这个是如果实现的呢?—— 这背后是因为 buildTypes 是 NamedDomainObjectContainer 类型,源码体现:

com.android.build.api.dsl.CommonExtension.kt

val buildTypes: NamedDomainObjectContainer<BuildType>

NamedDomainObjectContainer 的作用:

NamedDomainObjectContainer 直译是命名领域对象容器,是一个支持配置不固定数量配置的容器。主要功能分为 3 点:

Set 容器: 支持添加多个 T 类型对象,并且不允许命名重复;
命名 DSL: 支持以 DSL 的方式配置 T 类型对象,这也要求 T 类型必须带有 String name 属性,且必须带有以 String name 为参数的 public 构造函数;
SortSet 容器: 容器将保证元素以 name 自然顺序排序。

那么,以上配置相当于以下伪代码:

build.gradle

val buildTypes : Collections<BuildType>

BuildType release = BuildType("release")
release.minifyEnabled = false
release.proguardFiles = getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' BuildType debug = BuildType("debug")
... BuildType preview = BuildType("preview")
... buildTypes.add(release)
buildTypes.add(debug)
buildType.add(preview)

NamedDomainObjectContainer 的用法:

这里介绍一下具体用法,我们仅以你熟悉的 BuildType 为例,但不等于以下为源码。

1、定义类型 T: 在类型 T 中必须带有以 String name 为参数的 public 构造函数。例如:

BuildType.groovy

class BuildType {
// 必须有 String name 属性,且不允许构造后修改
@Nonnull
public final String name // 业务参数
boolean minifyEnabled BuildType(String name) {
this.name = name
}
}

2、定义 NamedDomainObjectContainer 属性: 在扩展类中定义一个 NamedDomainObjectContainer 类型属性。例如:

CommonExtension.grooyv

class CommonExtension {

    NamedDomainObjectContainer<BuildType> buildTypes

    CommonExtension(Project project) {
// 通过 project.container(...) 方法创建 NamedDomainObjectContainer
NamedDomainObjectContainer<BuildType> buildTypeObjs = project.container(BuildType)
buildTypes = buildTypeObjs
} // 嵌套扩展闭包函数,方法名为 buildTypes
void buildTypes(Action<NamedDomainObjectContainer<BuildType>> action) {
action.execute(buildTypes)
} void buildTypes(Closure closure) {
ConfigureUtil.configure(closure, buildTypes)
}
}

3、创建 Extension: 按照 4.1 节介绍的步骤创建扩展。例如:

project.extensions.create("android", CommonExtension)

到这里,就可以按照 buildTypes {} 的方式配置 BuildType 列表了。然而,你会发现每个配置项必须使用 = 进行赋值。这就有点膈应人了,有懂的大佬指导一下。

android {
buildTypes {
release {
// 怎样才能省略 = 号呢?
minifyEnabled = false
proguardFiles = getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

4. 插件调试

4.1 两个调试方法

在开发插件的过程一定需要调试,除了通过日志调试,我们也有断点调试的需求。这里总结两个方法:方法 1 虽然只支持调试简单执行任务,但已经能满足大部分需求,而且相对简单。而方法 2 支持命令行添加参数。

方法 1(简单): 直接提供 Android Studio 中 Gradle 面板的调试功能,即可调试插件。如下图,我们选择与插件功能相关的 Task,并右键选择 Debug 执行。

方法 2: 通过配置 IDE Configuration 以支持调试命令行任务,具体步骤:

1、创建 Remote 类型 Configuration:

2、执行命令: ./gradlew Task -Dorg.gradle.debug=true --no-daemon (开启 Debug & 不使用守护进程),执行后命令行会进入等待状态:

3、Attach Debug: 点击调试按钮,即可开始断点调试。

4.2 调试技巧

一些调试技巧:

引用插件源码: 在开发阶段可以直接本地依赖插件源码,而不需要将插件发布到 Maven 仓库,只需要在 build.gradle 文件中修改配置:

项目 build.gradle

buildscript {
repositories {
google()
jcenter()
}
dependencies {
// For Debug
classpath project(":easyupload")
// classpath "com.pengxr:easyupload:1.0.0"
}
...
}

插件代码开关: 由于 Plugin#apply 中的代码在配置阶段执行,如果其中的代码有问题就会出现 Sync 报错。又因为编译插件代码需要先 Sync,只能先将工程中所有使用插件的代码注释掉,重新编译插件模块,再将注释修改回来。真麻烦!我们还是加一个开关吧,例如:

gradle.properties

ENABLED=true

模块 build.gradle

if (ENABLED.toBoolean()) {
apply plugin: 'com.pengxr.easyupload'
upload {
name = "123"
}
}

5. 插件开发技巧总结

判断是否当前是 App 模块还是 Library 模块: 当我们开发 Android 项目相关插件时,经常需要根据插件的使用环境区分不同逻辑。例如插件应用在 App 模块和 Library 模块会采用不同逻辑。此时,我们可以用在 Plugin#apply() 中采用以下判断:

project.afterEvaluate {
// 1. Check if apply the ‘com.android.application’ plugin
if (!project.getPluginManager().hasPlugin("com.android.application")) {
return
}
}

插件开发语言: 最初,Groovy 是 Gradle 的首要语言,但随着 Java 和 Kotlin 语言的演进,这一现状有所改变。现在的趋势是:Gradle 脚本使用 Groovy 或 Kotlin 开发,而 Gradle 插件使用 Kotlin 开发。例如,我们可以发现 AGP 现在已经用 Kotlin 开发了。虽然趋势是往 Kotlin 靠,但目前存量的 Gradle 脚本 / 插件还是以 Groovy 为主。

Groovy 优势:社区沉淀、动态语言
Kotlin 优势:IDE 支持、趋势

原文: In general, a plugin implemented using Java or Kotlin, which are statically typed, will perform better than the same plugin implemented using Groovy.


6. 总结

到这里,Gradle 插件的部分就讲完了,需要 Demo 的同学可以看下我们之前实现过的小插件: EasyPrivacy。在本系列后续的文章中,也会有新的插件 Demo。关注我,带你了解更多,我们下次见。

参考资料

《实战 Gradle》—— [美] Benjamin Muschko 著,李建 朱本威 杨柳 译
《Gradle for Android》—— [美] Kevin Pelgrims 著,余小乐 译
Groovy 参考文档 —— Groovy 官方文档
Gradle 说明文档 —— Gradle 官方文档
Gradle DSL 参考文档 —— Gradle 官方文档
Developing Custom Gradle Plugins —— Gradle 官方文档
Using Gradle Plugins —— Gradle 官方文档
深入探索 Gradle 自动化构建技术(系列) —— jsonchao 著

手把手带你自定义 Gradle 插件 —— Gradle 系列(2)的相关教程结束。