开发自定义Gradle插件

Gradle插件是封装可重用的构建逻辑的一种方式,可以在许多不同的项目和构建中使用。Gradle允许您实现自己的插件,以便重用您的构建逻辑,并与他人共享。

您可以使用任何语言来实现Gradle插件,只要最终实现编译为JVM字节码即可。在我们的示例中,我们将在独立的插件项目中使用Java作为实现语言,在构建脚本插件示例中使用Groovy或Kotlin。通常情况下,使用静态类型的Java或Kotlin实现的插件将比使用Groovy实现的相同插件性能更好。

打包插件

有几个地方可以放置插件的源代码。

构建脚本

您可以直接在构建脚本中包含插件的源代码。这样做的好处是,插件会自动编译并包含在构建脚本的类路径中,无需进行任何操作。但是,插件在构建脚本之外不可见,因此您无法在定义插件的构建脚本之外重用该插件

buildSrc项目

您可以将插件的源代码放在rootProjectDir/buildSrc/src/main/java目录中(或rootProjectDir/buildSrc/src/main/groovy或rootProjectDir/buildSrc/src/main/kotlin,具体取决于您喜欢使用的语言)。Gradle会负责编译和测试插件,并使其在构建脚本的类路径上可用。该插件对构建使用的每个构建脚本都可见。但是,在构建之外不可见,因此您无法在定义该构建之外重用该插件。

有关buildSrc项目的更多详细信息,请参阅组织Gradle项目

独立项目

您可以为插件创建一个单独的项目。该项目生成并发布一个JAR文件,然后您可以在多个构建中使用该JAR文件并与他人共享。通常,此JAR文件可能包含一些插件,或将几个相关的任务类捆绑到一个库中。或者两者的组合。

在我们的示例中,为了保持简单,我们将从构建脚本中开始编写插件。然后我们将看看如何创建一个独立项目。

编写简单插件

要创建一个Gradle插件,您需要编写一个实现Plugin接口的类。当将插件应用于项目时,Gradle会创建插件类的实例,并调用实例的Plugin.apply()方法。将项目对象作为参数传递给插件,插件可以使用它来根据需要配置项目。

以下示例包含一个greeting插件,它向项目添加一个hello任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
build.gradle
class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
project.task('hello') {
doLast {
println 'Hello from the GreetingPlugin'
}
}
}
}

// Apply the plugin
apply plugin: GreetingPlugin

运行gradle -q hello命令的输出结果为:

1
Hello from the GreetingPlugin

需要注意的是,每次将插件应用于项目时都会创建一个新的插件实例。还要注意的是,Plugin类是一个泛型类型。该示例将Project类型作为类型参数传递给它。插件也可以接收Settings类型的参数,这样插件可以在设置脚本中应用,或者接收Gradle类型的参数,这样插件可以在初始化脚本中应用。

使插件可配置

大多数插件为构建脚本和其他插件提供一些配置选项,以自定义插件的工作方式。插件使用扩展对象来实现此功能。Gradle项目有一个关联的ExtensionContainer对象,该对象包含已应用于项目的插件的所有设置和属性。您可以通过向此容器添加扩展对象来为插件提供配置。扩展对象只是具有Java Bean属性的对象,表示配置。

让我们为项目添加一个简单的扩展对象。在这里,我们将一个greeting扩展对象添加到项目中,允许您配置问候语。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
build.gradle
interface GreetingPluginExtension {
Property<String> getMessage()
}

class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
// Add the 'greeting' extension object
def extension = project.extensions.create('greeting', GreetingPluginExtension)
extension.message.convention('Hello from GreetingPlugin')
// Add a task that uses configuration from the extension object
project.task('hello') {
doLast {
println extension.message.get()
}
}
}
}

apply plugin: GreetingPlugin

// Configure the extension
greeting.message = 'Hi from Gradle'

运行gradle -q hello命令的输出结果为:

1
Hi from Gradle

在此示例中,GreetingPluginExtension是一个具有名为message的属性的对象。使用名称greeting将扩展对象添加到项目中。然后,该对象作为具有与扩展对象相同名称的项目属性可用。

通常情况下,您可能需要在单个插件上指定几个相关属性。Gradle为每个扩展对象添加了一个配置块,因此您可以将设置分组在一起。下面的示例演示了这是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
build.gradle
interface GreetingPluginExtension {
Property<String> getMessage()
Property<String> getGreeter()
}

class GreetingPlugin implements Plugin<Project> {
void apply(Project project) {
def extension = project.extensions.create('greeting', GreetingPluginExtension)
project.task('hello') {
doLast {
println "${extension.message.get()} from ${extension.greeter.get()}"
}
}
}
}

apply plugin: GreetingPlugin

// Configure the extension using a DSL block
greeting {
message = 'Hi'
greeter = 'Gradle'
}

运行gradle -q hello命令的输出结果为:

1
Hi from Gradle

在此示例中,多个设置可以在greeting闭包中分组在一起。构建脚本中闭包块(greeting)的名称需要与扩展对象的名称匹配。然后,在执行闭包时,根据标准Groovy闭包委托特性,将扩展对象上的字段映射到闭包内的变量。

通过这种方式,使用扩展对象扩展了Gradle DSL,以添加一个项目属性和DSL块用于插件。并且由于扩展对象只是一个常规对象,因此可以通过向扩展对象添加属性和方法来提供自己的嵌套在插件块内的DSL。

开发项目扩展

有关实现项目扩展的更多信息,请参阅开发自定义Gradle类型

在自定义任务和插件中处理文件

在开发自定义任务和插件时,接受文件位置输入配置时,最好非常灵活。您应该使用Gradle的managed properties和project.layout来选择文件或目录位置。这样,实际位置仅在需要文件时解析,并且可以在构建配置期间的任何时间重新配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
build.gradle
abstract class GreetingToFileTask extends DefaultTask {

@OutputFile
abstract RegularFileProperty getDestination()

@TaskAction
def greet() {
def file = getDestination().get().asFile
file.parentFile.mkdirs()
file.write 'Hello!'
}
}

def greetingFile = objects.fileProperty()

tasks.register('greet', GreetingToFileTask) {
destination = greetingFile
}

tasks.register('sayGreeting') {
dependsOn greet
doLast {
def file = greetingFile.get().asFile
println "${file.text} (file: ${file.name})"
}
}

greetingFile.set(layout.buildDirectory.file('hello.txt'))

运行gradle -q sayGreeting命令的输出结果为:

1
Hello! (file: hello.txt)

在此示例中,我们将greet任务的destination属性配置为一个闭包/提供者,该闭包/提供者使用Project.file(java.lang.Object)方法在最后一刻将闭包/提供者的返回值转换为File对象。您会注意到,在上面的示例中,我们指定了greetingFile属性值,然后再配置使用它的任务。这种延迟评估是接受任何值设置文件属性的关键好处,然后在读取属性时解析该值。

将扩展属性映射到任务属性

通过扩展从构建脚本捕获用户输入,并将其映射到自定义任务的输入/输出属性,是一种很有用的模式。构建脚本作者只与扩展定义的DSL交互。插件实现中隐藏了命令式逻辑。

Gradle提供了一些类型,您可以在任务实现和扩展中使用这些类型来帮助您处理此问题。请参阅延迟配置获取更多信息。

独立项目

现在我们将把插件移动到一个独立的项目中,这样我们就可以发布并与他人共享。该项目只是一个生成包含插件类的JAR的Java项目。打包和发布插件的最简单且推荐的方式是使用Java Gradle插件开发插件。此插件会自动应用Java插件,将gradleApi()依赖项添加到api配置中,在生成的JAR文件中生成所需的插件描述符,并配置插件标记构件以供发布时使用。以下是该项目的简单构建脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
build.gradle
plugins {
id 'java-gradle-plugin'
}

gradlePlugin {
plugins {
simplePlugin {
id = 'org.example.greeting'
implementationClass = 'org.example.GreetingPlugin'
}
}
}

创建插件ID

插件ID应该是一个反向域名的组合,类似于Java包(例如:com.example.plugin)。这有助于避免冲突,并提供了一种将具有相似所有权的插件进行分组的方式。

插件ID应符合以下规则:

  • 可以包含任何字母数字字符、点号(.)和破折号(-)。
  • 必须包含至少一个点号(.)来分隔命名空间和插件名称。
  • 命名空间的惯例是使用小写的反向域名约定。
  • 名称中只能使用小写字符。
  • 不能使用org.gradle和com.gradleware命名空间。
  • 不能以点号(.)开头或结尾。
  • 不能包含连续的点号(..)。

虽然插件ID和包名之间存在常规的相似性,但是包名通常比插件ID更详细。例如,添加"gradle"作为插件ID的组成部分似乎是合理的,但由于插件ID只用于Gradle插件,这是多余的。通常,一个能够提供所有者信息和名称的命名空间就足够作为一个好的插件ID。

发布插件

如果要将插件发布到内部供组织内部使用,可以像发布其他代码构件一样进行发布。请参阅有关发布构件的Ivy和Maven章节。

如果您希望将插件发布供更广泛的Gradle社区使用,可以将其发布到Gradle插件门户网站(Gradle Plugin Portal)。该网站提供了搜索和收集Gradle社区贡献的插件信息的功能。请参考相关部分了解如何在该网站上发布您的插件。

在其他项目中使用插件

要在构建脚本中使用插件,需要在项目的设置文件的pluginManagement {}块中配置存储库。以下示例展示了当插件已发布到本地存储库时,如何进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
settings.gradle
pluginManagement {
repositories {
maven {
url = uri(repoLocation)
}
}
}
build.gradle
plugins {
id 'org.example.greeting' version '1.0-SNAPSHOT'
}

对于没有使用java-gradle-plugin发布的插件,请注意 如果您的插件是在没有使用Java Gradle插件开发插件的情况下发布的,则缺少Plugin Marker Artifact,这是插件DSL用于定位插件所需的。在这种情况下,在项目的设置文件的pluginManagement {}块中添加resolutionStrategy部分是解决另一个项目中的插件的推荐方式,如下所示。

1
2
3
4
5
6
7
8
settings.gradle
resolutionStrategy {
eachPlugin {
if (requested.id.namespace == 'org.example') {
useModule("org.example:custom-plugin:${requested.version}")
}
}
}

预编译的脚本插件

除了作为独立项目编写的插件外,Gradle还允许您提供以Groovy或Kotlin DSL编写的构建逻辑作为预编译的脚本插件。您可以将其编写为src/main/groovy目录中的*.gradle文件或src/main/kotlin目录中的*.gradle.kts文件。

预编译的脚本插件名称有两个重要限制

  • 不能以org.gradle开头。
  • 不能与内置插件ID相同。

这样可以确保预编译的脚本插件不会被忽略。

预编译的脚本插件会被编译成class文件,并打包到一个JAR中。从所有方面来看,它们都是二进制插件,可以按插件ID应用、测试和发布。实际上,它们的插件元数据是使用Gradle插件开发插件生成的。

使用Gradle 6.0构建的Kotlin DSL预编译的脚本插件无法与早期版本的Gradle一起使用。此限制将在未来的Gradle版本中解除。

从Gradle 6.4开始,可以使用Groovy DSL预编译的脚本插件。Groovy DSL预编译的脚本插件可应用于使用Gradle 5.0及更高版本的项目。

要应用预编译的脚本插件,您需要知道其ID,该ID派生自插件脚本的文件名(不包括.gradle扩展名)。

例如,src/main/groovy/java-library-convention.gradle脚本的插件ID将为java-library-convention。同样,src/main/groovy/my.java-library-convention.gradle将生成一个my.java-library-convention的插件ID。

为了演示如何实现和使用预编译的脚本插件,让我们以基于buildSrc项目的示例为例进行介绍。

首先,您需要一个buildSrc/build.gradle文件,该文件应用groovy-gradle-plugin插件:

1
2
3
4
buildSrc/build.gradle
plugins {
id 'groovy-gradle-plugin'
}

我们建议您还创建一个buildSrc/settings.gradle文件,该文件可以为空。

接下来,在buildSrc/src/main/groovy目录中创建一个新的java-library-convention.gradle文件,并将其内容设置为以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
buildSrc/src/main/groovy/java-library-convention.gradle
plugins {
id 'java-library'
id 'checkstyle'
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

checkstyle {
maxWarnings = 0
// ...
}

tasks.withType(JavaCompile) {
options.warnings = true
// ...
}

dependencies {
testImplementation("junit:junit:4.13")
// ...
}

此脚本插件简单地应用了Java Library和Checkstyle插件,并对其进行了配置。请注意,这实际上将插件应用于主项目,即应用预编译的脚本插件的项目。

最后,将脚本插件应用于根项目,如下所示:

1
2
3
4
build.gradle
plugins {
id 'java-library-convention'
}

在预编译的脚本插件中应用外部插件

要在预编译的脚本插件中应用外部插件,需要在插件项目的实现类路径中的插件构建文件中添加外部插件。

1
2
3
4
5
6
7
8
9
10
11
12
buildSrc/build.gradle
plugins {
id 'groovy-gradle-plugin'
}

repositories {
mavenCentral()
}

dependencies {
implementation 'com.bmuschko:gradle-docker-plugin:6.4.0'
}

然后可以在预编译的脚本插件中应用它。

1
2
3
4
buildSrc/src/main/groovy/my-plugin.gradle
plugins {
id 'com.bmuschko.docker-remote-api'
}

在这种情况下,插件版本在依赖声明中定义。

为插件编写测试

您可以使用ProjectBuilder类创建Project实例,以在测试插件实现时使用。

1
2
3
4
5
6
7
8
9
10
src/test/java/org/example/GreetingPluginTest.java
public class GreetingPluginTest {
@Test
public void greeterPluginAddsGreetingTaskToProject() {
Project project = ProjectBuilder.builder().build();
project.getPluginManager().apply("org.example.greeting");

assertTrue(project.getTasks().getByName("hello") instanceof GreetingTask);
}
}

更多细节

插件通常还提供自定义任务类型。有关更多详细信息,请参阅开发自定义Gradle任务类型

Gradle提供了一些在开发Gradle类型(包括插件)时非常有用的功能。有关更多详细信息,请参阅开发自定义Gradle类型

在开发Gradle插件时,谨慎记录构建日志非常重要。记录敏感信息(如凭据、令牌、某些环境变量)被视为安全漏洞。对于公共持续集成服务的构建日志是全球可见的,并可能泄露这些敏感信息。

背后的实现

那么Gradle如何找到插件的实现呢?答案是 - 您需要在JAR的META-INF/gradle-plugins目录中提供一个与您的插件ID匹配的属性文件,这由Java Gradle插件开发插件处理。

1
2
src/main/resources/META-INF/gradle-plugins/org.example.greeting.properties
implementation-class=org.example.GreetingPlugin

请注意,属性文件的名称与插件ID匹配,并放置在资源文件夹中,而implementation-class属性标识了插件实现类。

结合jacoco & spotless进行代码format

在build.gradle中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
plugins {
id "java"
id 'jacoco'
id "com.diffplug.spotless" version "6.22.0"
}

// version of dependencies
def jacocoToolVersion = "0.8.12"
def googleJavaFormatVersion = "1.17.0"

// ./gradlew spotlessApply
spotless {
java {
googleJavaFormat("${googleJavaFormatVersion}") // Google Java Formatter
target "**/src/main/java/**/*.java", "**/src/test/java/**/*.java"
}
}

tasks.named("build") {
dependsOn "spotlessApply"
}

jacoco {
toolVersion = "${jacocoToolVersion}"
}

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