开发Gradle plugin

最近在学习如何编写 Gradle plugin, 记录一下学习心得
插件分类
Gradle 插件有三种实现方式 Script plugins、Precomplied script plugins 和 Binary plugins
Script plugins: 在build.gradle中直接实现插件逻辑,只能在当前构建中使用。Precomplied script plugins: 在项目中的独立文件(.gradle或.gradle.kts)中实现插件逻辑,可以在项目中的多个构建中使用。Binary plugins: 使用独立项目实现插件逻辑,并打包为jar文件,在项目中通过引用jar文件来使用。
本文使用独立项目来制作 Binary plugins.
环境准备
需要安装 Java 和 Gradle
文中使用的是 Java 21.0.3 和 Gradle 8.10.2
初始化项目
gradle init --type java-gradle-plugin
Welcome to Gradle 8.10.2!
Here are the highlights of this release:
- Support for Java 23
- Faster configuration cache
- Better configuration cache reports
For more details see https://docs.gradle.org/8.10.2/release-notes.html
Starting a Gradle Daemon (subsequent builds will be faster)
Project name (default: gradle-plugin-demo):
Select build script DSL:
1: Kotlin
2: Groovy
Enter selection (default: Kotlin) [1..2]
Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no]
> Task :init
For more information, please refer to https://docs.gradle.org/8.10.2/userguide/custom_plugins.html in the Gradle documentation.
BUILD SUCCESSFUL in 1m 5s
1 actionable task: 1 executed
运行 init 命令之后会需要回答3个问题
Project name: 设置项目名字,(默认是当前目录的名字)Build script DSL:选择构建脚本的语言,默认是KotlinNew APIs and behavior: 是否启用新的API和特性,默认是no
gradle的项目都有一个构建文件 build.gradle 根据构建语言的不同文件的后缀名有所区别
Groovy 的是 build.gradle , Kotlin 的是 build.gradle.kts
初始化之后的目录结构如下:
.
├── .gitattributes
├── .gitignore
├── gradle
│ ├── libs.versions.toml
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── plugin
│ ├── build.gradle.kts
│ └── src
│ ├── functionalTest
│ │ └── java
│ │ └── org
│ │ └── example
│ │ └── GradlePluginDemoPluginFunctionalTest.java
│ ├── main
│ │ ├── java
│ │ │ └── org
│ │ │ └── example
│ │ │ └── GradlePluginDemoPlugin.java
│ │ └── resources
│ └── test
│ ├── java
│ │ └── org
│ │ └── example
│ │ └── GradlePluginDemoPluginTest.java
│ └── resources
└── settings.gradle.kts
19 directories, 12 files
grale: 配置gradle-wrapper用来定义项目使用的gradle的版本,libs.versions.toml是gradle的依赖版本管理文件是 [[Gradle版本目录(Version Catalogs)]] 的另一种实现方式plugin: 插件代码的目录,包含核心代码,测试代码settings.gradle.kts:Gradle的配置文件
Plugin id
我们可以在 build.gradle.kts 文件中看到如下内容
gradlePlugin {
// Define the plugin
val greeting by plugins.creating {
id = "org.example.greeting"
implementationClass = "org.example.GradlePluginDemoPlugin"
}
}
这里定义我们的插件的 id , 这个 id 就是插件的唯一标识。
入口类
public class GradlePluginDemoPlugin implements Plugin<Project> {
public void apply(Project project) {
// Register a task
project.getTasks().register("greeting", task -> {
task.doLast(s -> System.out.println("Hello from plugin 'org.example.greeting'"));
});
}
}
实现 Plugin<Project> 的类将作为插件的入口类 , 在一个插件在 Gradle 中被使用将会执行入口类的 apply(Project) 方法。
project.getTasks().register("greeting", task -> {
task.doLast(s -> System.out.println("Hello from plugin 'org.example.greeting'"));
});
通过 register 我们可以注册一个名字叫做 greeting 的 task 到 Gradle 的构建中. 这个task在运行的时候会在控制台输出 Hello from plugin 'org.example.greeting'
现在我们可以运行一下 org.example.GradlePluginDemoPluginFunctionalTest#canRunTask() 这个测试方法来看看一下实际效果
> Task :greeting
Hello from plugin 'org.example.greeting'
可以看到名字叫做 greeting 的插件被执行了,然后输出了 Hello form plugin 'org.example.greeting'
这就是我们通过 init 命令创建的 Gradle plugin 的 Hello world
Extension
当前我们的插件不能配置,只能输出固定的内容。Extension 可以让插件变的可配置。
Extension 允许插件向 project 对象添加自定义的配置块,从而在构建脚本中定义额外的配置选项或行为。
现在让我们给插件定义一个 Extension
package org.example;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.Property;
import javax.inject.Inject;
/**
* @author Too_young
*/
public class DemoExtension {
private final Property<String> greeting;
@Inject
public DemoExtension(ObjectFactory objects) {
this.greeting = objects.property(String.class).convention("Default greeting");
}
public Property<String> getGreeting() {
return greeting;
}
}
这是一个简单的 Extension 类,它只有一个属性 greeting,我们在初始化的时候给它设置了一个默认值 Default greeting.
[!tip]
Property<String>: 表示一个延迟计算的值ObjectFactory: 用以创建Gradle的各种类型的对象
延迟计算是Gradle的一个特性,它的作用就是在Property对象中的值在被调用的时候才会计算Property中的值。
我们有了一个 Extension , 现在我们需要将 Extension 加入到 Gradle 中,这样我们就能在构建文件(build.gradle)中使用它
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example;
import org.gradle.api.Project;
import org.gradle.api.Plugin;
/**
* A simple 'hello world' plugin.
*/
public class GradlePluginDemoPlugin implements Plugin<Project> {
public void apply(Project project) {
// add Extension
var extension = project.getExtensions().create("demo", DemoExtension.class);
var greeting = extension.getGreeting();
// Register a task
project.getTasks().register("greeting", task -> {
task.doLast(s -> System.out.println(greeting.get()));
});
}
}
在入口类中我们通过 project.getExtensions().create("demo", DemoExtension.class) 向 Gradle 中添加了一个名字为 demo 的 Extension。
关于延迟计算, 当代码运行到 greeting.get() 的时候,Gradle 才会开始计算 greeting 的值
现在我们的插件一个有了一个 Extension 并且已经加入到了 Gradle 中它的名字是 demo,我们来看已一下要如何使用这个 Extension .
plugins {
id("org.example.greeting")
}
demo {
greeting = "greeting from build.gradle.kts"
}
在 Gradle 的构建文件中添加插件的引用,我们就可以在文件中使用我们插件的 Extension.
让我们来测试一下,修改一下 GradlePluginDemoPluginFunctionalTest 来试试我们的 Extension 是否可用
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example;
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.io.FileWriter;
import java.nio.file.Files;
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 static org.junit.jupiter.api.Assertions.*;
/**
* A simple functional test for the 'org.example.greeting' plugin.
*/
class GradlePluginDemoPluginFunctionalTest {
@TempDir
File projectDir;
private File getBuildFile() {
return new File(projectDir, "build.gradle.kts");
}
private File getSettingsFile() {
return new File(projectDir, "settings.gradle");
}
@Test void canRunTask() throws IOException {
writeString(getSettingsFile(), "");
writeString(getBuildFile(),
"""
plugins { id("org.example.greeting") } demo {
greeting = "greeting from build.gradle.kts" } """);
// Run the build
GradleRunner runner = GradleRunner.create();
runner.forwardOutput();
runner.withPluginClasspath();
runner.withArguments("greeting");
runner.withProjectDir(projectDir);
BuildResult result = runner.build();
// Verify the result
assertTrue(result.getOutput().contains("greeting from build.gradle.kts"));
}
private void writeString(File file, String string) throws IOException {
try (Writer writer = new FileWriter(file)) {
writer.write(string);
} }
}
默认的测试类中定义的是 Groovy 语言的构建文件,我更喜欢用 kotlin 一些就改成了 build.gradle.kts.
现在我们运行一下看看效果
> Task :greeting
greeting from build.gradle.kts
BUILD SUCCESSFUL in 3s
抽象Task类
现在我们的插件可以通过 Extension 来进行配置了,但是我们插件的功能还只是输入,这可不能满足我们开发插件的初衷。
我们可以用通过继承 org.gradle.api.DefaultTask 来自定义任务。让我们来尝试一下
package org.example;
import org.gradle.api.DefaultTask;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.TaskAction;
import java.util.Objects;
/**
* @author Too_young
*/
public abstract class DemoTask extends DefaultTask {
public abstract Property<String> getGreeting();
@TaskAction
public void run() {
var greeting = getGreeting().get();
if (Objects.equals("Default greeting", greeting)) {
System.out.println("greeting not set!!!");
} else {
System.out.println(greeting);
}
}
}
通过 @TaskAction 来定义task的执行的核心逻辑,类似于task的入口方法。
@TaskAction 可以配置多个,但是 Gradle 不能保证其运行顺序,推荐只配置一个 @TaskAction
我们在task中设置一个判断,如果是默认的greeting就输出 greeting not set!!! , 如果不是就输出原内容
让我们来测试一下,还是修改一下 GradlePluginDemoPluginFunctionalTest 测试类
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* A simple functional test for the 'org.example.greeting' plugin.
*/
class GradlePluginDemoPluginFunctionalTest {
@TempDir
File projectDir;
private File getBuildFile() {
return new File(projectDir, "build.gradle.kts");
}
private File getSettingsFile() {
return new File(projectDir, "settings.gradle");
}
@Test void canRunTask() throws IOException {
writeString(getSettingsFile(), "");
writeString(getBuildFile(),
"""
plugins { id("org.example.greeting") } """);
// Run the build
GradleRunner runner = GradleRunner.create();
runner.forwardOutput();
runner.withPluginClasspath();
runner.withArguments("greeting");
runner.withProjectDir(projectDir);
BuildResult result = runner.build();
// Verify the result
assertTrue(result.getOutput().contains("greeting not set!!!"));
}
@Test void canExtension() throws IOException {
writeString(getSettingsFile(), "");
writeString(getBuildFile(),
"""
plugins { id("org.example.greeting") } demo {
greeting = "greeting from build.gradle.kts" } """);
// Run the build
GradleRunner runner = GradleRunner.create();
runner.forwardOutput();
runner.withPluginClasspath();
runner.withArguments("greeting");
runner.withProjectDir(projectDir);
BuildResult result = runner.build();
// Verify the result
assertTrue(result.getOutput().contains("greeting from build.gradle.kts"));
}
private void writeString(File file, String string) throws IOException {
try (Writer writer = new FileWriter(file)) {
writer.write(string);
} }
}
这里增加一个测试方法用来测试配置了 greeting 的情况,另一个方法用来测试 greeting 默认状态的情况
canRunTask() 的结果
> Task :greeting
greeting not set!!!
BUILD SUCCESSFUL in 1s
canExtension() 的结果
> Task :greeting
greeting from build.gradle.kts
BUILD SUCCESSFUL in 3s
到此我们已经成功的创建了一个 Gradle plugin 项目了。
Happy Coding!!!
