二进制插件指的是编译并作为 JAR 文件分发的插件。这些插件通常用 Java 或 Kotlin 编写,并为 Gradle 构建提供自定义功能或 task。

使用 Plugin Development 插件

可以使用 Gradle Plugin Development 插件 来辅助开发 Gradle 插件。

此插件将自动应用 Java Plugin,将 gradleApi() 依赖添加到 api 配置,在生成的 JAR 文件中生成所需的插件描述符,并配置 Plugin Marker Artifact 以在发布时使用。

要应用和配置插件,请将以下代码添加到您的构建文件

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

gradlePlugin {
    plugins {
        create("simplePlugin") {
            id = "org.example.greeting"
            implementationClass = "org.example.GreetingPlugin"
        }
    }
}
build.gradle
plugins {
    id 'java-gradle-plugin'
}

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

在开发插件时,建议编写和使用 自定义 task 类型,因为它能自动受益于 增量构建。将插件应用到您的项目的另一个好处是,validatePlugins task 会自动检查自定义 task 类型实现中定义的每个公共属性是否都存在输入/输出注解。

创建插件 ID

插件 ID 旨在全局唯一,类似于 Java 包名(即,反向域名)。这种格式有助于防止命名冲突,并允许对具有相似所有权的插件进行分组。

显式的插件标识符简化了将插件应用到项目的过程。您的插件 ID 应组合反映命名空间(指向您或您的组织的合理指针)和其提供的插件名称的组件。例如,如果您的 Github 帐户名为 foo,而您的插件名为 bar,则合适的插件 ID 可能是 com.github.foo.bar。同样,如果插件是在 baz 组织开发的,则插件 ID 可能是 org.baz.bar

插件 ID 应遵循以下准则

  • 可以包含任何字母数字字符、'.' 和 '-'。

  • 必须包含至少一个 '.' 字符,将命名空间与插件名称分隔开。

  • 按照惯例,命名空间使用小写反向域名约定。

  • 按照惯例,名称中仅使用小写字符。

  • 不得使用 org.gradlecom.gradlecom.gradleware 命名空间。

  • 不能以 '.' 字符开头或结尾。

  • 不能包含连续的 '.' 字符(即 '..')。

标识所有权和名称的命名空间对于插件 ID 来说就足够了。

当在单个 JAR 构件中捆绑多个插件时,建议遵循相同的命名约定。这种做法有助于逻辑地分组相关插件。

在单个项目中可以定义和注册(通过不同的标识符)的插件数量没有限制。

以类形式编写的插件的标识符应在包含插件类的项目的构建脚本中定义。为此,需要应用 java-gradle-plugin

buildSrc/build.gradle.kts
plugins {
    id("java-gradle-plugin")
}

gradlePlugin {
    plugins {
        create("androidApplicationPlugin") {
            id = "com.android.application"
            implementationClass = "com.android.AndroidApplicationPlugin"
        }
        create("androidLibraryPlugin") {
            id = "com.android.library"
            implementationClass = "com.android.AndroidLibraryPlugin"
        }
    }
}
buildSrc/build.gradle
plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        androidApplicationPlugin {
            id = 'com.android.application'
            implementationClass = 'com.android.AndroidApplicationPlugin'
        }
        androidLibraryPlugin {
            id = 'com.android.library'
            implementationClass = 'com.android.AndroidLibraryPlugin'
        }
    }
}

文件操作

在开发插件时,对于接受文件位置的输入配置,保持灵活性是一个好主意。

建议使用 Gradle 的 managed propertiesproject.layout 来选择文件或目录位置。这将启用延迟配置,以便仅在需要文件时才解析实际位置,并且可以在构建配置期间随时重新配置。

此 Gradle 构建文件定义了一个 task GreetingToFileTask,它将问候语写入文件。它还注册了两个 task:greet,它创建包含问候语的文件;以及 sayGreeting,它打印文件内容。greetingFile 属性用于指定问候语的文件路径

build.gradle.kts
abstract class GreetingToFileTask : DefaultTask() {

    @get:OutputFile
    abstract val destination: RegularFileProperty

    @TaskAction
    fun greet() {
        val file = destination.get().asFile
        file.parentFile.mkdirs()
        file.writeText("Hello!")
    }
}

val greetingFile = objects.fileProperty()

tasks.register<GreetingToFileTask>("greet") {
    destination = greetingFile
}

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

greetingFile = layout.buildDirectory.file("hello.txt")
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 = layout.buildDirectory.file('hello.txt')
$ gradle -q sayGreeting
Hello! (file: hello.txt)

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

您可以在 文件操作 中了解有关延迟文件操作的更多信息。

使用扩展使插件可配置

大多数插件为构建脚本和其他插件提供配置选项,以自定义插件的工作方式。插件使用扩展对象来做到这一点。

Project 有一个关联的 ExtensionContainer 对象,其中包含已应用于项目的插件的所有设置和属性。您可以通过将扩展对象添加到此容器来为您的插件提供配置。

扩展对象只是一个具有 Java Bean 属性的对象,表示配置。

让我们向项目添加一个 greeting 扩展对象,它允许您配置问候语

build.gradle.kts
interface GreetingPluginExtension {
    val message: Property<String>
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Add the 'greeting' extension object
        val extension = project.extensions.create<GreetingPluginExtension>("greeting")
        // Add a task that uses configuration from the extension object
        project.task("hello") {
            doLast {
                println(extension.message.get())
            }
        }
    }
}

apply<GreetingPlugin>()

// Configure the extension
the<GreetingPluginExtension>().message = "Hi from Gradle"
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)
        // 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
Hi from Gradle

在此示例中,GreetingPluginExtension 是一个具有名为 message 的属性的对象。扩展对象以名称 greeting 添加到项目中。此对象作为项目属性可用,其名称与扩展对象相同。the<GreetingPluginExtension>() 等同于 project.extensions.getByType(GreetingPluginExtension::class.java)

通常,您需要在单个插件上指定几个相关的属性。Gradle 为每个扩展对象添加一个配置块,因此您可以对设置进行分组

build.gradle.kts
interface GreetingPluginExtension {
    val message: Property<String>
    val greeter: Property<String>
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create<GreetingPluginExtension>("greeting")
        project.task("hello") {
            doLast {
                println("${extension.message.get()} from ${extension.greeter.get()}")
            }
        }
    }
}

apply<GreetingPlugin>()

// Configure the extension using a DSL block
configure<GreetingPluginExtension> {
    message = "Hi"
    greeter = "Gradle"
}
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
Hi from Gradle

在此示例中,多个设置可以在 configure<GreetingPluginExtension> 代码块中分组。 configure 函数用于配置扩展对象。它提供了一种方便的方式来设置属性或将配置应用于这些对象。构建脚本的 configure 函数 (GreetingPluginExtension) 中使用的类型必须与扩展类型匹配。然后,当代码块执行时,代码块的接收者是扩展。

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

声明 DSL 配置容器

使用扩展对象扩展了 Gradle DSL,为插件添加了项目属性和 DSL 代码块。由于扩展对象是常规对象,您可以通过向扩展对象添加属性和方法,在插件代码块内部提供您自己的嵌套 DSL。

让我们考虑以下构建脚本以进行说明。

build.gradle.kts
plugins {
    id("org.myorg.server-env")
}

environments {
    create("dev") {
        url = "https://127.0.0.1:8080"
    }

    create("staging") {
        url = "http://staging.enterprise.com"
    }

    create("production") {
        url = "http://prod.enterprise.com"
    }
}
build.gradle
plugins {
    id 'org.myorg.server-env'
}

environments {
    dev {
        url = 'https://127.0.0.1:8080'
    }

    staging {
        url = 'http://staging.enterprise.com'
    }

    production {
        url = 'http://prod.enterprise.com'
    }
}

插件公开的 DSL 公开了一个容器,用于定义一组环境。用户配置的每个环境都有一个任意但声明性的名称,并用其自己的 DSL 配置代码块表示。上面的示例实例化了开发、暂存和生产环境,包括其各自的 URL。

每个环境都必须在代码中具有数据表示形式以捕获值。环境的名称是不可变的,可以作为构造函数参数传入。目前,数据对象存储的唯一其他参数是 URL。

以下 ServerEnvironment 对象满足这些要求

ServerEnvironment.java
abstract public class ServerEnvironment {
    private final String name;

    @javax.inject.Inject
    public ServerEnvironment(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    abstract public Property<String> getUrl();
}

Gradle 公开了工厂方法 ObjectFactory.domainObjectContainer(Class, NamedDomainObjectFactory) 来创建数据对象的容器。该方法接受的参数是表示数据的类。可以通过将其添加到具有特定名称的扩展容器中,将创建的 NamedDomainObjectContainer 类型的实例公开给最终用户。

插件通常会在插件实现中后处理捕获的值,例如,配置 task

ServerEnvironmentPlugin.java
public class ServerEnvironmentPlugin implements Plugin<Project> {
    @Override
    public void apply(final Project project) {
        ObjectFactory objects = project.getObjects();

        NamedDomainObjectContainer<ServerEnvironment> serverEnvironmentContainer =
            objects.domainObjectContainer(ServerEnvironment.class, name -> objects.newInstance(ServerEnvironment.class, name));
        project.getExtensions().add("environments", serverEnvironmentContainer);

        serverEnvironmentContainer.all(serverEnvironment -> {
            String env = serverEnvironment.getName();
            String capitalizedServerEnv = env.substring(0, 1).toUpperCase() + env.substring(1);
            String taskName = "deployTo" + capitalizedServerEnv;
            project.getTasks().register(taskName, Deploy.class, task -> task.getUrl().set(serverEnvironment.getUrl()));
        });
    }
}

在上面的示例中,为每个用户配置的环境动态创建了一个部署 task。

您可以在 开发自定义 Gradle 类型 中找到有关实现项目扩展的更多信息。

建模类似 DSL 的 API

插件公开的 DSL 应该是可读且易于理解的。

例如,让我们考虑插件提供的以下扩展。在其当前形式中,它提供了一个“扁平”的属性列表,用于配置网站的创建

build-flat.gradle.kts
plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = "https://gradle.org.cn"
    vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}
build-flat.gradle
plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = 'https://gradle.org.cn'
    vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
}

随着公开属性数量的增加,您应该引入嵌套的、更具表现力的结构。

以下代码片段添加了一个名为 siteInfo 的新配置块,作为扩展的一部分。这更强烈地表明了这些属性的含义

build.gradle.kts
plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    siteInfo {
        websiteUrl = "https://gradle.org.cn"
        vcsUrl = "https://github.com/gradle/gradle-site-plugin"
    }
}
build.gradle
plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    siteInfo {
        websiteUrl = 'https://gradle.org.cn'
        vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
    }
}

为此类扩展实现后备对象很简单。首先,引入一个新的数据对象来管理属性 websiteUrlvcsUrl

SiteInfo.java
abstract public class SiteInfo {

    abstract public Property<String> getWebsiteUrl();

    abstract public Property<String> getVcsUrl();
}

在扩展中,创建 siteInfo 类的实例和一个将捕获的值委托给数据实例的方法。

要配置底层数据对象,请定义 Action 类型的参数。

以下示例演示了在扩展定义中使用 Action

SiteExtension.java
abstract public class SiteExtension {

    abstract public RegularFileProperty getOutputDir();

    @Nested
    abstract public SiteInfo getSiteInfo();

    public void siteInfo(Action<? super SiteInfo> action) {
        action.execute(getSiteInfo());
    }
}

将扩展属性映射到 task 属性

插件通常使用扩展来捕获来自构建脚本的用户输入,并将其映射到自定义 task 的输入/输出属性。构建脚本作者与扩展的 DSL 交互,而插件实现处理底层逻辑

app/build.gradle.kts
// Extension class to capture user input
class MyExtension {
    @Input
    var inputParameter: String? = null
}

// Custom task that uses the input from the extension
class MyCustomTask : org.gradle.api.DefaultTask() {
    @Input
    var inputParameter: String? = null

    @TaskAction
    fun executeTask() {
        println("Input parameter: $inputParameter")
    }
}

// Plugin class that configures the extension and task
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Create and configure the extension
        val extension = project.extensions.create("myExtension", MyExtension::class.java)
        // Create and configure the custom task
        project.tasks.register("myTask", MyCustomTask::class.java) {
            group = "custom"
            inputParameter = extension.inputParameter
        }
    }
}
app/build.gradle
// Extension class to capture user input
class MyExtension {
    @Input
    String inputParameter = null
}

// Custom task that uses the input from the extension
class MyCustomTask extends DefaultTask {
    @Input
    String inputParameter = null

    @TaskAction
    def executeTask() {
        println("Input parameter: $inputParameter")
    }
}

// Plugin class that configures the extension and task
class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        // Create and configure the extension
        def extension = project.extensions.create("myExtension", MyExtension)
        // Create and configure the custom task
        project.tasks.register("myTask", MyCustomTask) {
            group = "custom"
            inputParameter = extension.inputParameter
        }
    }
}

在此示例中,MyExtension 类定义了一个可以在构建脚本中设置的 inputParameter 属性。MyPlugin 类配置此扩展并使用其 inputParameter 值来配置 MyCustomTask task。然后,MyCustomTask task 在其逻辑中使用此输入参数。

您可以在 延迟配置 中了解有关可以在 task 实现和扩展中使用的类型的更多信息。

使用约定添加默认配置

插件应在特定上下文中提供合理的默认值和标准,从而减少用户需要做出的决策数量。使用 project 对象,您可以定义默认值。这些被称为约定

约定是使用默认值初始化的属性,用户可以在其构建脚本中覆盖这些属性。例如

build.gradle.kts
interface GreetingPluginExtension {
    val message: Property<String>
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Add the 'greeting' extension object
        val extension = project.extensions.create<GreetingPluginExtension>("greeting")
        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<GreetingPlugin>()
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
$ gradle -q hello
Hello from GreetingPlugin

在此示例中,GreetingPluginExtension 是一个表示约定的类。message 属性是约定属性,默认值为 'Hello from GreetingPlugin'。

用户可以在其构建脚本中覆盖此值

build.gradle.kts
GreetingPluginExtension {
    message = "Custom message"
}
build.gradle
GreetingPluginExtension {
    message = 'Custom message'
}
$ gradle -q hello
Custom message

将能力与约定分离

在插件中将能力与约定分离允许用户选择要应用哪些 task 和约定。

例如,Java Base 插件提供非倾向性(即,通用)功能,如 SourceSets,而 Java 插件添加 Java 开发人员熟悉的 task 和约定,如 classesjarjavadoc

在设计您自己的插件时,请考虑开发两个插件 - 一个用于能力,另一个用于约定 - 以向用户提供灵活性。

在下面的示例中,MyPlugin 包含约定,而 MyBasePlugin 定义能力。然后,MyPlugin 应用 MyBasePlugin,这被称为插件组合。要从另一个插件应用插件

MyBasePlugin.java
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyBasePlugin implements Plugin<Project> {
    public void apply(Project project) {
        // define capabilities
    }
}
MyPlugin.java
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPluginManager().apply(MyBasePlugin.class);

        // define conventions
    }
}

对插件做出反应

Gradle 插件实现中的一种常见模式是配置构建中现有插件和 task 的运行时行为。

例如,插件可以假设它应用于基于 Java 的项目,并自动重新配置标准源目录

InhouseStrongOpinionConventionJavaPlugin.java
public class InhouseStrongOpinionConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        // Careful! Eagerly appyling plugins has downsides, and is not always recommended.
        project.getPluginManager().apply(JavaPlugin.class);
        SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
        SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
        main.getJava().setSrcDirs(Arrays.asList("src"));
    }
}

这种方法的缺点是它会自动强制项目应用 Java 插件,对其施加强烈的倾向性(即,降低灵活性和通用性)。在实践中,应用插件的项目甚至可能不处理 Java 代码。

插件可以对使用项目应用 Java 插件这一事实做出反应,而不是自动应用 Java 插件。只有在这种情况下,才会应用某些配置

InhouseConventionJavaPlugin.java
public class InhouseConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPluginManager().withPlugin("java", javaPlugin -> {
            SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
            SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
            main.getJava().setSrcDirs(Arrays.asList("src"));
        });
    }
}

如果没有充分的理由假设使用项目具有预期的设置,则对插件做出反应优于应用插件。

相同的概念适用于 task 类型

InhouseConventionWarPlugin.java
public class InhouseConventionWarPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getTasks().withType(War.class).configureEach(war ->
            war.setWebXml(project.file("src/someWeb.xml")));
    }
}

对构建特性做出反应

插件可以访问构建中构建特性的状态。Build Features API 允许检查用户是否请求了特定的 Gradle 特性,以及它是否在当前构建中处于活动状态。构建特性的一个例子是 配置缓存

主要有两种用例

  • 在报告或统计信息中使用构建特性的状态。

  • 通过禁用不兼容的插件功能,逐步采用实验性的 Gradle 特性。

下面是一个插件示例,它利用了这两种情况。

对构建特性做出反应
public abstract class MyPlugin implements Plugin<Project> {

    @Inject
    protected abstract BuildFeatures getBuildFeatures(); (1)

    @Override
    public void apply(Project p) {
        BuildFeatures buildFeatures = getBuildFeatures();

        Boolean configCacheRequested = buildFeatures.getConfigurationCache().getRequested() (2)
            .getOrNull(); // could be null if user did not opt in nor opt out
        String configCacheUsage = describeFeatureUsage(configCacheRequested);
        MyReport myReport = new MyReport();
        myReport.setConfigurationCacheUsage(configCacheUsage);

        boolean isolatedProjectsActive = buildFeatures.getIsolatedProjects().getActive() (3)
            .get(); // the active state is always defined
        if (!isolatedProjectsActive) {
            myOptionalPluginLogicIncompatibleWithIsolatedProjects();
        }
    }

    private String describeFeatureUsage(Boolean requested) {
        return requested == null ? "no preference" : requested ? "opt-in" : "opt-out";
    }

    private void myOptionalPluginLogicIncompatibleWithIsolatedProjects() {
    }
}
1 BuildFeatures 服务可以注入到插件、task 和其他托管类型中。
2 访问特性的 requested 状态以进行报告。
3 使用特性的 active 状态来禁用不兼容的功能。

构建特性属性

BuildFeature 状态属性用 Provider<Boolean> 类型表示。

构建特性的 BuildFeature.getRequested() 状态确定用户是否请求启用或禁用该特性。

requested provider 值为

  • true — 用户选择使用该特性

  • false — 用户选择不使用该特性

  • undefined — 用户既没有选择使用也没有选择不使用该特性

构建特性的 BuildFeature.getActive() 状态始终是已定义的。它表示特性在构建中的有效状态。

active provider 值为

  • true — 该特性可能会以特定于该特性的方式影响构建行为

  • false — 该特性不会影响构建行为

请注意,active 状态不依赖于 requested 状态。即使用户请求了特性,由于构建中使用的其他构建选项,它也可能仍然不活动。即使用户未指定首选项,Gradle 也可以默认激活特性。

使用自定义 dependencies 代码块

插件可以在自定义代码块中提供依赖声明,从而允许用户以类型安全和上下文感知的方式声明依赖。

例如,自定义 dependencies 代码块允许插件选择一个有意义的名称,该名称可以一致地使用,而不是用户需要知道和使用底层的 Configuration 名称来添加依赖。

添加自定义 dependencies 代码块

要添加自定义 dependencies 代码块,您需要创建一个新的 类型,它将表示用户可用的依赖范围集。新类型需要可以从插件的一部分(从域对象或扩展)访问。最后,依赖范围需要连接回将在依赖解析期间使用的底层 Configuration 对象。

有关如何在 Gradle 核心插件中使用此功能的示例,请参阅 JvmComponentDependenciesJvmTestSuite

1. 创建一个扩展 Dependencies 的接口

您还可以扩展 GradleDependencies 以访问 Gradle 提供的依赖,如 gradleApi()
ExampleDependencies.java
/**
 * Custom dependencies block for the example plugin.
 */
public interface ExampleDependencies extends Dependencies {

2. 为依赖范围添加访问器

对于您的插件要支持的每个依赖范围,添加一个返回 DependencyCollector 的 getter 方法。

ExampleDependencies.java
    /**
     * Dependency scope called "implementation"
     */
    DependencyCollector getImplementation();

3. 为自定义 dependencies 代码块添加访问器

为了使自定义 dependencies 代码块可配置,插件需要添加一个返回上面新类型的 getDependencies 方法和一个名为 dependencies 的可配置代码块方法。

按照惯例,您的自定义 dependencies 代码块的访问器应称为 getDependencies()/dependencies(Action)。此方法可以命名为其他名称,但用户需要知道不同的代码块可以像 dependencies 代码块一样工作。

ExampleExtension.java
    /**
     * Custom dependencies for this extension.
     */
    @Nested
    ExampleDependencies getDependencies();

    /**
     * Configurable block
     */
    default void dependencies(Action<? super ExampleDependencies> action) {
        action.execute(getDependencies());
    }

4. 将依赖范围连接到 Configuration

最后,插件需要将自定义 dependencies 代码块连接到一些底层的 Configuration 对象。如果未完成此操作,则自定义代码块中声明的任何依赖都将不可用于依赖解析。

ExamplePlugin.java
        project.getConfigurations().dependencyScope("exampleImplementation", conf -> {
            conf.fromDependencyCollector(example.getDependencies().getImplementation());
        });
在此示例中,用户将用于添加依赖的名称是 “implementation”,但底层的 Configuration 被命名为 exampleImplementation
build.gradle.kts
example {
    dependencies {
        implementation("junit:junit:4.13")
    }
}
build.gradle
example {
    dependencies {
        implementation("junit:junit:4.13")
    }
}

自定义 dependencies 代码块与顶级 dependencies 代码块之间的差异

每个依赖范围都返回一个 DependencyCollector,它提供强类型方法来添加和配置依赖。

还有一个 DependencyFactory,它具有工厂方法,可从不同的表示法创建新依赖。可以使用这些工厂方法延迟创建依赖,如下所示。

自定义 dependencies 代码块与顶级 dependencies 代码块的不同之处在于以下几点

  • 必须使用 StringDependency 的实例、FileCollectionDependencyProviderMinimalExternalModuleDependencyProviderConvertible 来声明依赖。

  • 在 Gradle 构建脚本之外,您必须显式调用 DependencyCollector 的 getter 和 add

    • dependencies.add(\"implementation\", x) 变为 getImplementation().add(x)

  • 您不能使用 Kotlin 和 Java 中的 Map 表示法声明依赖。请在 Kotlin 和 Java 中改用多参数方法。

    • Kotlin: compileOnly(mapOf(\"group\" to \"foo\", \"name\" to \"bar\")) 变为 compileOnly(module(group = \"foo\", name = \"bar\"))

    • Java: compileOnly(Map.of(\"group\", \"foo\", \"name\", \"bar\")) 变为 getCompileOnly().add(module(\"foo\", \"bar\", null))

  • 您不能使用 Project 的实例添加依赖。您必须先将其转换为 ProjectDependency

  • 您不能直接添加版本目录 bundle。请改为在每个配置上使用 bundle 方法。

    • Kotlin 和 Groovy: implementation(libs.bundles.testing) 变为 implementation.bundle(libs.bundles.testing)

  • 您不能直接为非 Dependency 类型使用 providers。而是使用 DependencyFactory 将它们映射到 Dependency

    • Kotlin 和 Groovy: implementation(myStringProvider) 变为 implementation(myStringProvider.map { dependencyFactory.create(it) })

    • Java: implementation(myStringProvider) 变为 getImplementation().add(myStringProvider.map(getDependencyFactory()::create)

  • 与顶层 dependencies 代码块不同,constraints 不在单独的代码块中。

    • 而是通过使用 constraint(…​) 装饰依赖项来添加 constraints,例如 implementation(constraint("org:foo:1.0"))

请记住,dependencies 代码块可能无法提供与 顶层 dependencies 代码块 相同的方法。

插件应首选通过它们自己的 dependencies 代码块添加依赖项。

提供默认依赖项

插件的实现有时需要使用外部依赖项。

您可能希望使用 Gradle 的依赖管理机制自动下载一个 artifact,然后在插件中声明的任务类型的 action 中使用它。理想情况下,插件实现不需要用户询问该依赖项的坐标 - 它可以简单地预定义一个合理的默认版本。

让我们看一个插件示例,该插件下载包含数据的文件以进行进一步处理。插件实现声明了一个自定义配置,该配置允许使用依赖项坐标分配这些外部依赖项

DataProcessingPlugin.java
public class DataProcessingPlugin implements Plugin<Project> {
    public void apply(Project project) {
        Configuration dataFiles = project.getConfigurations().create("dataFiles", c -> {
            c.setVisible(false);
            c.setCanBeConsumed(false);
            c.setCanBeResolved(true);
            c.setDescription("The data artifacts to be processed for this plugin.");
            c.defaultDependencies(d -> d.add(project.getDependencies().create("org.myorg:data:1.4.6")));
        });

        project.getTasks().withType(DataProcessing.class).configureEach(
            dataProcessing -> dataProcessing.getDataFiles().from(dataFiles));
    }
}
DataProcessing.java
abstract public class DataProcessing extends DefaultTask {

    @InputFiles
    abstract public ConfigurableFileCollection getDataFiles();

    @TaskAction
    public void process() {
        System.out.println(getDataFiles().getFiles());
    }
}

这种方法对于最终用户来说很方便,因为无需主动声明依赖项。插件已经提供了有关此实现的所有详细信息。

但是,如果用户想要重新定义默认依赖项怎么办?

没问题。插件还公开了自定义配置,该配置可用于分配不同的依赖项。实际上,默认依赖项被覆盖了

build.gradle.kts
plugins {
    id("org.myorg.data-processing")
}

dependencies {
    dataFiles("org.myorg:more-data:2.6")
}
build.gradle
plugins {
    id 'org.myorg.data-processing'
}

dependencies {
    dataFiles 'org.myorg:more-data:2.6'
}

您会发现这种模式非常适用于需要在执行任务的 action 时使用外部依赖项的任务。您可以更进一步,通过公开扩展属性(例如 JaCoCo 插件 中的 toolVersion)来抽象用于外部依赖项的版本。

最大限度地减少外部库的使用

在您的 Gradle 项目中使用外部库可以带来极大的便利,但请注意,它们可能会引入复杂的依赖关系图。Gradle 的 buildEnvironment 任务可以帮助您可视化这些依赖关系,包括插件的依赖关系。请记住,插件共享同一个类加载器,因此同一库的不同版本可能会发生冲突。

为了演示,我们假设以下构建脚本

build.gradle.kts
plugins {
    id("org.asciidoctor.jvm.convert") version "4.0.2"
}
build.gradle
plugins {
    id 'org.asciidoctor.jvm.convert' version '4.0.2'
}

任务的输出清楚地表明了 classpath 配置的类路径

$ gradle buildEnvironment

> Task :buildEnvironment

------------------------------------------------------------
Root project 'external-libraries'
------------------------------------------------------------

classpath
\--- org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:4.0.2
     \--- org.asciidoctor:asciidoctor-gradle-jvm:4.0.2
          +--- org.ysb33r.gradle:grolifant-rawhide:3.0.0
          |    \--- org.tukaani:xz:1.6
          +--- org.ysb33r.gradle:grolifant-herd:3.0.0
          |    +--- org.tukaani:xz:1.6
          |    +--- org.ysb33r.gradle:grolifant40:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.apache.commons:commons-collections4:4.4
          |    |    +--- org.ysb33r.gradle:grolifant-core:3.0.0
          |    |    |    +--- org.tukaani:xz:1.6
          |    |    |    +--- org.apache.commons:commons-collections4:4.4
          |    |    |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant50:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant40-legacy-api:3.0.0
          |    |         +--- org.tukaani:xz:1.6
          |    |         +--- org.apache.commons:commons-collections4:4.4
          |    |         +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    |         \--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant60:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant50:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant70:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant50:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant60:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant80:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant50:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant60:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant70:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          +--- org.asciidoctor:asciidoctor-gradle-base:4.0.2
          |    \--- org.ysb33r.gradle:grolifant-herd:3.0.0 (*)
          \--- org.asciidoctor:asciidoctorj-api:2.5.7

(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Gradle 插件不在其自己的隔离类加载器中运行,因此您必须考虑是否真的需要库,或者更简单的解决方案是否足够。

对于作为任务执行一部分执行的逻辑,请使用 Worker API,它允许您隔离库。

提供插件的多个变体

插件的变体指的是插件的不同风格或配置,这些风格或配置是为特定需求或用例量身定制的。这些变体可以包括基本插件的不同实现、扩展或配置。

配置其他插件变体最方便的方法是使用 feature variants,这是所有应用 Java 插件之一的 Gradle 项目中都可用的概念

dependencies {
    implementation 'com.google.guava:guava:30.1-jre'        // Regular dependency
    featureVariant 'com.google.guava:guava-gwt:30.1-jre'    // Feature variant dependency
}

在以下示例中,每个插件变体都是独立开发的。为每个变体编译单独的 source set 并将其打包到单独的 jar 中。

以下示例演示了如何添加与 Gradle 7.0+ 兼容的变体,而“main”变体与旧版本兼容

build.gradle.kts
val gradle7 = sourceSets.create("gradle7")

java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) (1)
    }
}

configurations.configureEach {
    if (isCanBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
                objects.named("7.0"))
        }
    }
}

tasks.named<Copy>(gradle7.processResourcesTaskName) { (3)
    val copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into("META-INF/gradle-plugins")
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    "gradle7CompileOnly"(gradleApi()) (4)
}
build.gradle
def gradle7 = sourceSets.create('gradle7')

java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) (1)
    }
}

configurations.configureEach {
    if (canBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
                      objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}

tasks.named(gradle7.processResourcesTaskName) { (3)
    def copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into('META-INF/gradle-plugins')
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    gradle7CompileOnly(gradleApi()) (4)
}
只有 Gradle 7 或更高版本才能显式地被变体作为目标,因为对此的支持仅在 Gradle 7 中添加。

首先,我们为我们的 Gradle 7 插件变体声明一个单独的 source set 和一个 feature variant。然后,我们进行一些特定的连接,将该 feature 变成一个合适的 Gradle 插件变体

1 与组件 GAV 相对应的隐式 capability 分配给该变体。
2 Gradle API 版本属性 分配给我们 Gradle7 变体的所有 可消费的配置。Gradle 使用此信息来确定在插件解析期间选择哪个变体。
3 配置 processGradle7Resources 任务以确保插件描述符文件被添加到 Gradle7 变体 Jar 中。
4 为我们的新变体添加对 gradleApi() 的依赖,以便在编译时 API 可见。

请注意,目前没有方便的方法可以访问您正在构建插件所用的 Gradle 版本的 API。理想情况下,每个变体都应该能够声明对其支持的最低 Gradle 版本的 API 的依赖。这将在未来得到改进。

上面的代码片段假设您的插件的所有变体都具有相同位置的插件类。也就是说,如果您的插件类是 org.example.GreetingPlugin,您需要在 src/gradle7/java/org/example 中创建该类的第二个变体。

使用多变体插件的版本特定变体

给定对多变体插件的依赖,当 Gradle 解析以下任何内容时,它将自动选择最匹配当前 Gradle 版本的变体:

最佳匹配变体是目标 Gradle API 版本最高且不超过当前构建的 Gradle 版本的变体。

在所有其他情况下,如果存在不指定支持的 Gradle API 版本的插件变体,则首选该变体。

在使用插件作为依赖项的项目中,可以请求插件依赖项的支持不同 Gradle 版本的变体。这允许依赖于其他插件的多变体插件访问其 API,这些 API 专门在其版本特定的变体中提供。

此代码片段使 上面定义的插件变体 gradle7 使用其对其他多变体插件的依赖项的匹配变体

build.gradle.kts
configurations.configureEach {
    if (isCanBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named("7.0"))
        }
    }
}
build.gradle
configurations.configureEach {
    if (canBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}