第五十八章. 编写自定义插件

Chapter 58. Writing Custom Plugins

Gradle 插件打包了构建逻辑的可复用部分,这些逻辑可以在许多不同的项目和构建中使用。Gradle 允许你实现自己的自定义插件,因此你可以复用自己的构建逻辑,并与他人共享。
A Gradle plugin packages up reusable pieces of build logic, which can be used across many different projects and builds. Gradle allows you to implement your own custom plugins, so you can reuse your build logic, and share it with others.

你可以任选一种自己喜欢的语言来实现自定义插件,只要这个实现最终能够被编译成字节码。这里我们以 Groovy 为例,你也可以使用 Java 或 Scala。
You can implement a custom plugin in any language you like, provided the implementation ends up compiled as bytecode. For the examples here, we are going to use Groovy as the implementation language. You could use Java or Scala instead, if you want.

58.1. 打包插件

58.1. Packaging a plugin

有几个地方可以放插件的源码。
There are several places where you can put the source for the plugin.

构建脚本
Build script

你可以直接在构建脚本中包含插件源码。好处是你不需要再做什么,这个插件就会自动编译并加入构建脚本的类路径中。然而,在这个构建脚本之外,插件是不可见的,因此你不能够在你定义这个插件的脚本以外的地方来复用它。
You can include the source for the plugin directly in the build script. This has the benefit that the plugin is automatically compiled and included in the classpath of the build script without you having to do anything. However, the plugin is not visible outside the build script, and so you cannot reuse the plugin outside the build script it is defined in.

buildSrc 项目
buildSrc project

你可以将插件源码放在 rootProjectDir/buildSrc/src/main/rootvy 目录中。Gradle 将负责编译和测试插件,并使其在构建脚本的类路径中可用。该插件对构建所使用的每个构建脚本都是可见的。但是,它在构建外部不可见,因此你无法在定义它的构建之外的地方复用这个插件。
You can put the source for the plugin in the rootProjectDir/buildSrc/src/main/groovy directory. Gradle will take care of compiling and testing the plugin and making it available on the classpath of the build script. The plugin is visible to every build script used by the build. However, it is not visible outside the build, and so you cannot reuse the plugin outside the build it is defined in.

有关 buildSrc 项目的更详细信息,请参阅《第五十九章,组织构建逻辑》。
See Chapter 59, Organizing Build Logic for more details about the buildSrc project.

独立项目
Standalone project

你可以为你的插件创建单独的项目。这个项目会生成和发布一个 JAR,随后你可以在多个构建中使用它并分享出去。通常,这个 JAR 可能包含一些自定义插件,或者将多个相关的任务类捆绑到一个库中,或两者皆有。
You can create a separate project for your plugin. This project produces and publishes a JAR which you can then use in multiple builds and share with others. Generally, this JAR might include some custom plugins, or bundle several related task classes into a single library. Or some combination of the two.

在我们的例子中,我们将从在构建脚本中定义插件开始,以保持简单。随后我们会着眼于创建一个独立的项目。
In our examples, we will start with the plugin in the build script, to keep things simple. Then we will look at creating a standalone project.

58.2. 编写一个简单的插件

58.2. Writing a simple plugin

要创建自定义插件,你需要编写一个 Plugin 的实现。当插件用于项目时, Gradle 将实例化插件并调用插件实例的 Plugin.apply() 方法。项目对象将作为参数传进去,插件可使用该参数来配置项目。以下示例包含了一个 greeting 插件,其向项目添加了一个 hello 任务。
To create a custom plugin, you need to write an implementation of Plugin. Gradle instantiates the plugin and calls the plugin instance's Plugin.apply() method when the plugin is used with a project. The project object is passed as a parameter, which the plugin can use to configure the project however it needs to. The following sample contains a greeting plugin, which adds a hello task to the project.

示例 58.1. 自定义插件 - Example 58.1. A custom plugin

build.gradle

apply plugin: GreetingPlugin

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.task('hello') << {
            println "Hello from the GreetingPlugin"
        }
    }
}

gradle -q hello 的输出结果
Output of gradle -q hello

> gradle -q hello
Hello from the GreetingPlugin

需要注意的一点是,对于一个给定的插件,每一个应用它的项目都会创建一个新的实例。
One thing to note is that a new instance of a given plugin is created for each project it is applied to.

58.3. 从构建中获取输入

58.3. Getting input from the build

大多数插件需要从构建脚本获取一些配置。其中一种实现的方法是使用扩展对象。Gradle Project 有一个相关联的 ExtensionContainer 对象,该对象能帮助跟踪传给插件的所有设置和属性。你可以通过告知扩展容器有关插件的信息来捕获用户输入。只要将符合 Java Bean 的类添加到扩展容器的扩展列表中,你就可以获取输入。对于一个插件而言,Groovy 是一种不错的语言选择,因为普通的旧 Groovy 对象就包含了 Java Bean 需要的所有 getter 和 setter 方法。
Most plugins need to obtain some configuration from the build script. One method for doing this is to use extension objects. The Gradle Project has an associated ExtensionContainer object that helps keep track of all the settings and properties being passed to plugins. You can capture user input by telling the extension container about your plugin. To capture input, simply add a Java Bean compliant class into the extension container's list of extensions. Groovy is a good language choice for a plugin because plain old Groovy objects contain all the getter and setter methods that a Java Bean requires.

让我们向项目添加一个简单的扩展对象。这里我们向项目添加了一个 greeting 扩展对象,它可以让从外配置问候语。
Let's add a simple extension object to the project. Here we add a greeting extension object to the project, which allows you to configure the greeting.

示例 58.2. 一个自定义插件扩展 - Example 58.2. A custom plugin extension

build.gradle

apply plugin: GreetingPlugin

greeting.message = 'Hi from Gradle'

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

class GreetingPluginExtension {
    def String message = 'Hello from GreetingPlugin'
}

gradle -q hello 的输出结果
Output of gradle -q hello

> gradle -q hello
Hi from Gradle

在这个示例中,GreetingPluginExtension 是一个普通的旧 Groovy 对象,它有一个叫 message的字段。这个扩展对象以 greeting 名字添加到插件列表中。然后该对象变为一个有效的项目属性,属性名称与这个扩展对象相同。
In this example, GreetingPluginExtension is a plain old Groovy object with a field called message. The extension object is added to the plugin list with the name greeting. This object then becomes available as a project property with the same name as the extension object.

通常情况下,你会有几个相关联的属性需要在一个插件上指定。Gradle 为每个扩展对象添加了一个配置闭包块,因此你可以把这些设置分组。下面是其如何使用的例子。
Oftentimes, you have several related properties you need to specify on a single plugin. Gradle adds a configuration closure block for each extension object, so you can group settings together. The following example shows you how this works.

示例 58.3. 有配置闭包的自定义插件 - Example 58.3. A custom plugin with configuration closure

build.gradle

apply plugin: GreetingPlugin

greeting {
    message = 'Hi'
    greeter = 'Gradle'
}

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

class GreetingPluginExtension {
    String message
    String greeter
}

gradle -q hello 的输出结果
Output of gradle -q hello

> gradle -q hello
Hi from Gradle

在此示例中,可以将多个设置分组到 greeting 闭包中。构建脚本中的这个闭包块的名字(greeting)需要与扩展对象的名字匹配。然后,当执行闭包时,扩展对象上的字段将根据标准 Groovy 闭包委托功能映射到闭包内的变量上。
In this example, several settings can be grouped together within the greeting closure. The name of the closure block in the build script (greeting) needs to match the extension object name. Then, when the closure is executed, the fields on the extension object will be mapped to the variables within the closure based on the standard Groovy closure delegate feature.

58.4. 在自定义任务和插件中使用文件

58.4. Working with files in custom tasks and plugins

在开发自定义任务和插件时,文件位置如果能够配置将使插件非常灵活。为此,你可以利用 Project.file() 方法尽可能晚地将值解析为文件。
When developing custom tasks and plugins, it's a good idea to be very flexible when accepting input configuration for file locations. To do this, you can leverage the Project.file() method to resolve values to files as late as possible.

示例 58.4. 文件属性的惰性评估 - Example 58.4. Evaluating file properties lazily

build.gradle

class GreetingToFileTask extends DefaultTask {

    def destination

    File getDestination() {
        project.file(destination)
    }

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

task greet(type: GreetingToFileTask) {
    destination = { project.greetingFile }
}

task sayGreeting(dependsOn: greet) << {
    println file(greetingFile).text
}

ext.greetingFile = "$buildDir/hello.txt"

gradle -q sayGreeting 的输出结果
Output of gradle -q sayGreeting

> gradle -q sayGreeting
Hello!

在这个例子中,我们将 greet 任务的 destination 属性作为闭包配置,它将在最后通过 Project.file() 方法将闭包中的返回值转换为一个文件对象。你会注意到,在上面例子中,我们是配置在任务中使用 greettingFile 属性之后才指定它的值。这种惰性求值的主要好处是在设置文件属性时它能够接收任何值,然后在读取属性时才解析这个值。
In this example, we configure the greet task destination property as a closure, which is evaluated with the Project.file() method to turn the return value of the closure into a file object at the last minute. You will notice that in the example above we specify the greetingFile property value after we have configured to use it for the task. This kind of lazy evaluation is a key benefit of accepting any value when setting a file property, then resolving that value when reading the property.

58.5. 独立项目

58.5. A standalone project

现在我们将插件移到一个独立的项目中,这样我们就可以发布它,并与他人共享。这个项目只是一个 Groovy 项目,其将生成包含插件类的 JAR。以下是该项目的一个简单的构建脚本。它应用了 Groovy 插件,并将 Gradle API 添加为编译时依赖。
Now we will move our plugin to a standalone project, so we can publish it and share it with others. This project is simply a Groovy project that produces a JAR containing the plugin classes. Here is a simple build script for the project. It applies the Groovy plugin, and adds the Gradle API as a compile-time dependency.

示例 58.5. 对自定义插件的构建 - Example 58.5. A build for a custom plugin

build.gradle

apply plugin: 'groovy'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

注意:此例子的代码可以在 Gradle 的二进制文件或源码里的 samples/customPlugin/plugin 中看到。
Note: The code for this example can be found at samples/customPlugin/plugin which is in both the binary and source distributions of Gradle.


那么,Gradle 是如何找到 Plugin 的实现的?答案是你需要在 jar 文件里的 META-INF/gradle-plugins 目录中提供一个属性文件,并且文件名要和插件名相匹配。
So how does Gradle find the Plugin implementation? The answer is you need to provide a properties file in the jar's META-INF/gradle-plugins directory that matches the name of your plugin.

示例 58.6. 连接自定义插件 - Example 58.6. Wiring for a custom plugin

src/main/resources/META-INF/gradle-plugins/greeting.properties

implementation-class=org.gradle.GreetingPlugin

注意,属性的文件名要和插件名相匹配,放在资源文件夹里,并且 implementation-class 属性要指定 Plugin 的实现类。
Notice that the properties filename matches the plugin's name and is placed in the resources folder, and that the implementation-class property identifies the Plugin implementation class.

58.5.1. 在另一个项目中使用你的插件

58.5.1. Using your plugin in another project

要在构建脚本中使用一个插件,你需要将这个插件添加到构建脚本的类路径中。对此,你可以使用《第 59.5 节,“构建脚本的外部依赖”》中所述的 buildscript { } 块。以下示例展示了当包含插件的 JAR 已经发布到本地仓库时,如何执行此操作:
To use a plugin in a build script, you need to add the plugin classes to the build script's classpath. To do this, you use a buildscript { } block, as described in Section 59.5, “External dependencies for the build script”. The following example shows how you might do this when the JAR containing the plugin has been published to a local repository:

示例 58.7. 在另一个项目中使用一个自定义插件 - Example 58.7. Using a custom plugin in another project

build.gradle

buildscript {
    repositories {
        maven {
            url uri('../repo')
        }
    }
    dependencies {
        classpath group: 'org.gradle', name: 'customPlugin', version: '1.0-SNAPSHOT'
    }
}
apply plugin: 'greeting'

58.5.2. 为插件编写测试

58.5.2. Writing tests for your plugin

你可以使用 ProjectBuilder 类来创建 Project 实例,以在测试插件的实现时使用。
You can use the ProjectBuilder class to create Project instances to use when you test your plugin implementation.

示例 58.8. 测试自定义插件 - Example 58.8. Testing a custom plugin

src/test/groovy/org/gradle/GreetingPluginTest.groovy

class GreetingPluginTest {
    @Test
    public void greeterPluginAddsGreetingTaskToProject() {
        Project project = ProjectBuilder.builder().build()
        project.apply plugin: 'greeting'

        assertTrue(project.tasks.hello instanceof GreetingTask)
    }
}

58.6. 维护多个域对象

58.6. Maintaining multiple domain objects

Gradle 提供了一些维护对象集合的实用工具类,能够良好地在 Gradle 构建语言中使用。
Gradle provides some utility classes for maintaining collections of object, which work well with the Gradle build language.

示例 58.9. 管理域对象 - Example 58.9. Managing domain objects

build.gradle

apply plugin: DocumentationPlugin

books {
    quickStart {
        sourceFile = file('src/docs/quick-start')
    }
    userGuide {

    }
    developerGuide {

    }
}

task books << {
    books.each { book ->
        println "$book.name -> $book.sourceFile"
    }
}

class DocumentationPlugin implements Plugin<Project> {
    void apply(Project project) {
        def books = project.container(Book)
        books.all {
            sourceFile = project.file("src/docs/$name")
        }
        project.extensions.books = books
    }
}

class Book {
    final String name
    File sourceFile

    Book(String name) {
        this.name = name
    }
}

gradle -q books 的输出结果
Output of gradle -q books

> gradle -q books
developerGuide -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/developerGuide
quickStart -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/quick-start
userGuide -> /home/user/gradle/samples/userguide/organizeBuildLogic/customPluginWithDomainObjectContainer/src/docs/userGuide

Project.container() 方法创建了 NamedDomainObjectContainer的实例,这些实例有许多用于管理和配置对象的有用方法。为能使用任意一种 project.container 方法的类型,必须公开一个“name”属性,作为对象的唯一且不变的名称。容器方法的 project.container(Class) 变体通过尝试调用该类只有一个 string 参数的构造函数来创建新实例,该参数是这个对象所想要的名称。请参阅以上链接,以查看 project.container 允许自定义实例化策略的重载方法。
The Project.container() methods create instances of NamedDomainObjectContainer, that have many useful methods for managing and configuring the objects. In order to use a type with any of the project.container methods, it MUST expose a property named “name” as the unique, and constant, name for the object. The project.container(Class) variant of the container method creates new instances by attempting to invoke the constructor of the class that takes a single string argument, which is the desired name of the object. See the above link for project.container method variants that allow custom instantiation strategies.