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

Chapter 58. Writing Custom Plugins

Gradle 插件打包了可以复用的构建逻辑块,这些逻辑可以在不同的项目和构建中使用。Gradke 允许你实现你自己的自定义插件,因此你可以重用你的构建逻辑,并且与他人分享。
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.

你可以使用你喜欢的任何一种语言来实现一个自定义插件,只要这个实现最终能够提供编译的字节码。在这里的例子中,我们将使用Gradle来作为实现语言。如果你想的话,你也可以使用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/groovy 目录中。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接口。当这个插件被用在一个project上时,Gradle会实例化这个插件,并且调用这个插件实例的 Plugin.apply() 方法。这个project对象会被作为一个参数传进去,该参数可以让插件用于对这个project配置它所需要的东西。下面的例子包含了一个问候语插件,它把一个 hello 任务添加到project中。
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 ProjectExtensionContainer 对象会有关联,该对象可以帮助追踪传给插件的所有配置和属性。通过你的插件相关的扩展容器,你可以获取用户的输入。要获取输入,只需将一个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.

让我们向项目中添加一个简单的扩展对象。在这里,我们向该project添加了一个 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 是一个plain old 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
}

greetingFile = "$buildDir/hello.txt"

Output of gradle -q sayGreeting

> gradle -q sayGreeting
Hello!

在这个例子中,我们配置了 greet 任务的 destination 属性为一个闭包, 它将在最后通过Project.file() 方法将闭包中的返回值转为一个文件对象。你会注意到,在上面的例子中,我们是在已经配置了在作用中使用 greetingFile 属性之后才指定它的值。这种懒评估的主要好处是当设置文件属性时它能够接收任何值,并在读取这个属性的时候去解析这个值。
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

注意,属性的文件名要和插件名相匹配,放在resources夹里,并且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

要在一个构建脚本中使用一个插件,你需要先将这个插件的类添加到构建脚本的classpath中。要做到这一点,你要使用在第 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.