Gradle插件的分类

许多Gradle功能都是通过添加插件(Plugin)来完成,例如插件可以添加新的任务(JavaCompile)、域对象(sourceSet)、约定(Java源码位于src/main/java文件下)等等功能。其实插件(Plugin)也可以分为两种常见的分类:脚本插件(Script Plugin)和二进制插件(Binary Plugin)。
二进制插件可以通过实现 Plugin 接口的方式编写,也可以通过 GroovyKotlin DSL 中的声明来编写。它们可以驻留在构建脚本、项目层次结构中,也可以驻留在外部的插件 jar 中。脚本插件是附加的构建脚本,可进一步配置构建,并且通常实现声明性方法来操作构建。它们通常在构建中使用,但可以外部化和远程访问。

脚本插件(Script Plugin)

Gradle的语境下,除去init.gradle(.kts)build.gradle(.kts)setting.gradle(.kts)脚本,其他脚本均视为插件,也就是脚本插件(Script Plugin)。这些脚本插件又可细分为两类:

  • 独立脚本插件(Standalone Script Plugin): 无特定的脚本存放点,只要是可被基础脚本索引到的独立脚本,都可称为独立脚本插件。
  • 预编译脚本插件(Precompiled Script Plugin): 需要将脚本放置到一个独立的模块,然后将其编译产物加入构建环境的依赖中,根据常见的存放点和引入方式,我们可以分为独立工程模块和buildSrc模块两种。

假设有这么一个需求:对Android的各个模块统一配置lint相关的参数。通过配置对比了解两种脚本的异同。

预编译脚本插件

首先看预编译脚本如何实现的,预编译脚本需要放入到独立的模块,为了方便放入到buildSrc内:

–Project
I—app
I—build.gradle.kts
I—buildSrc
I—src
I—main
I—kotlin
I—com.modi.introduction.buildSrc
I—lib-convention-script-plugin.gradle.kts

1
2
3
4
5
6
7
8
9
10
plugins{
id("com.android.application")
}
android{
lint {
abortOnError=false
}
}

println("this lib-convention-script-plugin.gradle.kts from ./buildSrc is applied")

从上面看和我在模块中直接配置好像没什么区别,为什么放到buildSrc中的脚本就成了预编译脚本呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plugins {
`kotlin-dsl`
`java-gradle-plugin`
}
//...

repositories {
google()
}

dependencies {
//...
implementation("com.android.tools.build:gradle:7.2.2")
}

实时上其中奥秘就是buildSrc模块下build.gradle.kts引入的kotlin-dsl插件:它引入了kotlin系列插件、Java-gradle-pluginprecompiled- script-plugin的系列插件,覆盖了Kotlin编译器插件和Gradle插件。脚本经过编译会生成一个普通的二进制插件,通过apply(...)方法包裹了其具体内容,预编译脚本插件的名称也由此而来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Precompiled [lib-convention-script-plugin.gradle.kts][Lib_convention_script_plugin_gradle] script plugin.
*
* @see Lib_convention_script_plugin_gradle
*/
class LibConventionScriptPluginPlugin : org.gradle.api.Plugin<org.gradle.api.Project> {
override fun apply(target: org.gradle.api.Project) {
try {
Class
.forName("Lib_convention_script_plugin_gradle")
.getDeclaredConstructor(org.gradle.api.Project::class.java, org.gradle.api.Project::class.java)
.newInstance(target, target)
} catch (e: java.lang.reflect.InvocationTargetException) {
throw e.targetException
}
}
}

另外为了让独立脚本插件能够引用AGP配置,需要在buildSrcbuild.gralde.kts中引入相关依赖

独立脚本插件

接下来实现通过的功能看看独立脚本插件下如何配置

Project
** I—app**
** I—build.gradle.kts**
** I—standalone-scripts**
** I—lib-convention-script-plugin2.gradle.kts**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.android.build.gradle.LibraryExtension
buildscript{
repositories{
mavenCentral()
google()
gradlePluginPortal()
}

dependencies {
classpath("com.android.tools.build:gradle:7.2.2")
}

}

apply(plugin="com.android.library")

configure<LibraryExtension>{
lint{
abortOnError=false
}

}

println("The lib-convention-script-plugin2 from ./standalone-scripts is applied.")

独立脚本插件是不依赖任何项目,知识单纯文件,有很多局限性,如果想要达到插件的效果,需要做一些配置工作。

  1. 如果脚本需要用到任何依赖,都需要在自身的的buildscript{}dsl配置里声明,例如在这就单独加了AGP的依赖
  2. 它不支持plugins{id(...)}的方式引入插件,必须使用apply(plugin="...")
  3. 由于apply(plugin="...")不支持类型安全的模型访问器,所以无法直接使用android{}的配置入口,取而代之的是原生Gradle的API configure(…)

Android 的模块中引入独立脚本插件:

1
apply(from = "../standalone-scripts/lib-convention-script-plugin2.gradle.kts")


Gradle Groovy DSL 中,没有上述的限制,所以大家在Groovy的情况下往往直接使用独立脚本插件。但是在Gradle Kotlin DSL的环境下,官方推荐使用预编译脚本插件,统一的管理和类型安全的模型访问器能给开发中带来便利。
独立脚本插件和预编译插件总结:

独立脚本插件(standalone script plugin) 预编译脚本插件(precompiled script plugin)
指定目录 buildSrc或其他独立模块,该模块需要引入kotlin-dsl插件
被引入的方式 只能通过apply(from=””)方式引入到脚本中 支持plugins(id(“”))和
apply(from=””)
引入外部插件 只能通过apply(from=””)方式 支持plugins(id(“”))和
apply(from=””)
引入外部依赖 使用buildscript(…)进行独立声明 基于项目自身的build.gradle.kts声明的依赖环境
类型安全的模型访问器 不支持 支持

二进制插件(Binary Plugin)

日常开发中我们看到的二进制插件(Binary Plugin)一般都是XXXPlugin.kt等类或者文件载体,其内部的代码实现了org.gradle.api.Plugin的类。注意并非因为它是一个.kt文件,才可以称之为二进制插件(Binary Plugin),在脚本插件(Script Plugin)中同样可以显示声明一个这样的类:

1
2
3
4
5
6
7
8
9
//buildSrc/src/main/kotlin/dummy-script-plugin.gradle.kts
class DummyBinaryPluginInScript:Plugin<Project>{
//这里的参数不能用project,会和脚本自身环境中project冲突
override fun apply(target: Project) {
println("DummyBinaryPluginInScript")
}
}
apply<DummyBinaryPluginInScript>()

在这个环境下,我们只能通过apply引用插件。而plugin{id(..)}的方式则需要借助完整的工程结构和java-gradle-plugin来实现。完整的工程结构同预编译脚步插件:需要将这些类放置到一个独立的模块,然后将其编译产物加入构建环境的依赖中,根据常见的存放点和引入方式,分为独立工程模块和buildSrc模块两种。

我们先看一个简单的二进制插件(Binary Plugin):

1
2
3
4
5
6
//buildSrc/src/main/kotlin/.../LibConventionBinaryPlugin
class LibConventionBinaryPlugin:Plugin<Project> {
override fun apply(target: Project) {
println("The lib-convention-binary-plugin ")
}
}

来到buildSrc中的build.gralde.kts:

1
2
3
4
5
6
gradlePlugin {
plugins.register("lib-convention-binary-plugin"){
id="lib-convention-binary-plugin"
implementationClass="com.modi.introduction.buildsrc.LibConventionBinaryPlugin"
}
}

gradlePlugin{…}的配置用于注册一系列二进制插件到输出的jar包中,你可以在下述路径中发现它的身影:build/resources/main/META-INF/gradle-plugins/lib-convention-binary-plugin.properties。这是典型的一种Service Provider Interface(SPI)的做法,Gradle在加载了该模块后,会搜素MATE-INF/gradle-plugins区域的文件,把符合条件的插件注册到当前可使用的插件列表。它和Google的AutoService工具的作用类似,通过插件描述或者注解标记等方法生产资源描述文件,减少手动创建资源描述的工作。

1
2
//lib-convention-binary-plugin.properties
implementation-class=com.modi.introduction.buildsrc.LibConventionBinaryPlugin

参考:

  1. Using Gradle Plugins
  2. Developing Custom Gradle Plugins
  3. Android 构建与架构实战