Gradle系列5--自定义Gradle插件

探讨Gradle插件的自定义有助更好的理解Gradle,本文综合了userguide中的几篇相关文档,将不单独给出链接(除非很有必要),主要有:

Gradle插件中创建可重用构建逻辑的过程,然后可以将其应用于其他Gradle构建。Gradle的核心提供了构建任何东西的基础架构,但插件允许构建脚本作者以最少的努力完成工作,使他们能够专注于构建的内容,而不是如何构建。插件可以应用约定,添加新任务类型,与第三方工具和库集成等。例如,java插件提供标准的源目录布局和任务,例如compileJava和compileTestJava。

Gradle插件打包可重用的构建逻辑片段,可用于许多不同的项目和构建。Gradle允许自定义插件,可以重用构建逻辑,并与其他人分享。可以使用任何语言实现Gradle插件,前提是实现最终能编译为字节码。示例中,我们将使用Groovy作为实现语言。Groovy,Java或Kotlin都是用于实现插件的语言的不错选择,因为Gradle API已经设计为可以很好地使用这些语言。通常,使用Java或Kotlin实现的静态类型的插件将比使用Groovy实现的相同插件执行得更好。

打包插件

可以放置Gradle插件代码的几个地方:

  • 构建脚本
    直接在build.gradle中包含代码, 自动编译. 对外不可见,不可重用.

  • buildSrc 工程
    插件代码放在 rootProjectDir/buildSrc/src/main/groovy 目录. Gradle 编译、测试插件,并添加到在build.gradle的classpath中. 对每个构建脚本可见. 对外不可见,不可重用.

  • 独立工程
    独立的插件程序, 生成发布一个可供多个构建共享的JAR. 一般 JAR可以包含一些插件或任务.

简单插件

要创建Gradle插件,需要编写一个实现Plugin接口的类。当插件应用于项目时,Gradle会创建插件类的实例并调用实例的Plugin.apply()方法。项目对象作为参数传递,插件可以使用它来自由配置项目。以下示例包含greeting插件,该插件向项目添加hello任务。

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输出:

> gradle -q hello
Hello from the GreetingPlugin

需要注意的一点是,应用它的每个项目都会创建一个新的插件实例。Plugin是泛型类型,示例使其接收Project类型作为类型参数。插件可以改为接收类型为应用于设置脚本中的Settings或初始化脚本中的Gradle类型的参数。

插件可配置

大多数插件需要从构建脚本中获取一些配置,一种作法是使用扩展对象。Gradle的Project关联一个包含已应用于项目的插件的所有设置和属性ExtensionContainer对象,通过向该容器添加扩展对象来为插件提供配置。扩展对象只是一个Java Bean兼容类。Groovy是实现扩展对象的很好选择,因为普通的旧Groovy对象包含Java Bean所需的所有getter和setter方法,Java和Kotlin也是不错的选择。让我们为项目添加一个简单的扩展对象。这里添加一个可以配置问候语的 greeting扩展对象。

build.gradle

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

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        // 添加 'greeting' 扩展对象
        def extension = project.extensions.create('greeting', GreetingPluginExtension)
        // 添加一个使用扩展对象配置的task
        project.task('hello') {
            doLast {
                println extension.message
            }
        }
    }
}

apply plugin: GreetingPlugin

// 配置扩展
greeting.message = 'Hi from Gradle'

gradle -q hello输出

> gradle -q hello
Hi from Gradle

示例中,GreetingPluginExtension是一个旧的Groovy对象,有一个message的属性。扩展对象被添加到greeting的插件列表中,而后该对象成为项目的一个与扩展对象同名的有效属性。以上面为例,Project中添加一个greeting扩展就相当于给Project配置了一个greeting属性。通常需要在单个插件上指定几个相关属性。Gradle为每个扩展对象添加了一个配置闭包块(Closure block),因此您可以将设置组合在一起。以下示例显示了它的工作原理。

build.gradle

class GreetingPluginExtension {
  String message
  String greeter
}

...
        println "${extension.message} from ${extension.greeter}"
...

apply plugin: GreetingPlugin

// 使用DSL块配置扩展
greeting {
    message = 'Hi'
    greeter = 'Gradle'
}

gradle -q hello输出

> gradle -q hello
Hi from Gradle

这里在greeting闭包内将多个设置组合在一起。构建脚本中闭包块名称(greeting)需要与扩展对象名称匹配。当闭包执行时,扩展对象上的字段将根据标准Groovy闭包委托功能映射到闭包内的变量。

buildSrc工程

复杂的构建逻辑通常封装为自定义任务或二进制插件。自定义任务和插件实现不应该存在于构建脚本中。只要代码不需要在多个独立项目之间共享,就可以非常方便地使用buildSrc实现。目录buildSrc被视为已包含的构建。一旦发现该目录,Gradle会自动编译并测试代码并将其放入构建脚本的classpath中。对于多模块构建,只能有一个buildSrc目录,且必须位于根项目目录下。buildSrc应该优先于脚本插件,因为它更容易维护,重构和测试代码。buildSrc适用于Java和Groovy项目的相同源代码约定,还提供对Gradle API的直接访问。可以在buildSrc下的build.gradle中声明其他依赖项。

$ mkdir gradle-plugin-buildsrc
$ cd gradle-plugin-buildsrc/
$ gradle wrapper --gradle-version 4.6 --distribution-type all #gradle-4.6-all初始化Wrapper
$ ./gradlew init    #初始化根工程
$ mkdir buildSrc    
$ cd buildSrc/
$ ../gradlew init --type groovy-library #buildSrc初始化为groovy-library
$ rm -r gradle* sett* src/*/*/*.groovy  #删除无用文件

添加buildSrc/src/main/groovy/org/example/greeting/GreetingPlugin.groovy

package org.example.greeting

import org.gradle.api.Plugin
import org.gradle.api.Project

class GreetingPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.tasks.create("hello", Greeting.class) { task ->
            task.setMessage("Hello")
            task.setRecipient("World")
        }
    }
}

添加buildSrc/src/main/groovy/org/example/greeting/Greeting.groovy

package org.example.greeting

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class Greeting extends DefaultTask {
    private String message
    private String recipient

    String getMessage() { return message }

    void setMessage(String message) { this.message = message }

    String getRecipient() { return recipient }

    void setRecipient(String recipient) { this.recipient = recipient }

    @TaskAction
    void sayGreeting() {
        println "$message, $recipient!"
    }
}

根工程build.gradle加入

apply plugin: org.example.greeting.GreetingPlugin

后执行

$ ./gradlew hello
Hello, World!

我这里执行的时候抛出了一个buildSrc下的异常:

Caused by: groovy.lang.GroovyRuntimeException: Conflicting module versions. Module [groovy-all is loaded in version 2.4.12 and you are trying to load version 2.4.13

groovy-all版本改成2.4.12就可以了

另外由于buildSrc工程能被构建任务自动识别, 因此build.gradle中的内容完全可以删除,只保留额外的依赖和配置, 这样也不会有上面版本冲突的问题

分配插件id

大多数情况使用id以其容易记忆会比使用全类名来的方便,还会使构建文件更整洁。下面声明插件的id。
创建buildSrc/src/main/resources/META-INF/gradle-plugins/org.example.greeting.properties

implementation-class=org.example.greeting.GreetingPlugin

Gradle使用这个文件来确定哪个类实现了Plugin接口。文件名去掉.properties后缀就是插件的id。根工程build.gradle改成

apply plugin: 'org.example.greeting'

plugins {
    id 'org.example.greeting'
}

注意上面是如何将org.example.greeting.properties文件映射到插件id的。

独立工程

现在将我们的插件移到一个独立的项目中,可以发布它并与其他人分享。这是一个Groovy项目,它生成一个包含插件类的JAR。这是项目的简单构建脚本。它应用Groovy插件,并将Gradle API添加为编译时依赖项。

$ mv buildSrc plugin

此时plugin模块不再享有buildSrc时也有的自动被构建脚本识别、自动添加gradle/groovy等依赖、自动添加到classpath中这些“特权”, 因此这些工作需要插件作者来做。编辑plugin/build.gradle:

  1. 识别工程

    顶级settings.gradle加入include ':plugin'

  2. 添加依赖

    plugins {
        id 'groovy'
    }
    
    dependencies {
        compile gradleApi()
        compile localGroovy()
    }
    
  3. 分配一个id

    如果之前buildSrc中没有分配id这里的作法和buildSrc分配插件id是一样的, 这里重新进行配置。
    创建 plugin/src/main/resources/META-INF/gradle-plugins/org.samples.greeting.properties

    implementation-class=org.example.greeting.GreetingPlugin
    

    到这里还不能使用插件。顶级build.gradle要到哪里去加载插件jar包呢? 对了classpath没有配置,还要继续配置。

  4. 发布插件
    发布插件的方式常见的有maven、ivy和本地这三种仓库, 前两个属于中央仓库,提交产品需要审核,这里采用便捷的本地maven仓库。编辑plugin/build.gradle加入

    plugins {
      id 'maven'
    }
    
    uploadArchives{
        repositories.mavenDeployer{
            repository(url:uri('../repo')) //保存到上一级目录的repo下
            pom.project{
                groupId 'org.gradle'
                artifactId 'customPlugin'
                version '1.0'
            }
        }
    }
    
  5. 注释掉顶级build.gradle下应用的插件, 根目录执行./gradlew uploadArchives

    执行完毕后工程顶级目录下有个repo目录:

    gradle-plugin-buildsrc/
    ├── build.gradle
    ...
    ├── plugin
    ├── repo
    └── settings.gradle
    
    repo/
    └── org
        └── gradle
            └── customPlugin
                ├── 1.0
                │   ├── customPlugin-1.0.jar
                │   ├── customPlugin-1.0.jar.md5
                │   ├── customPlugin-1.0.jar.sha1
                │   ├── customPlugin-1.0.pom
                │   ├── customPlugin-1.0.pom.md5
                │   └── customPlugin-1.0.pom.sha1
                ├── maven-metadata.xml
                ├── maven-metadata.xml.md5
                └── maven-metadata.xml.sha1
    
  6. 添加插件成品到顶级build.gradle的classpath中

    buildscript {
        repositories {
            maven {
                url uri('repo')
            }
        }
        
        dependencies{
            classpath 'org.gradle:customPlugin:1.0'
        }
    }
    
  7. 新建一个模块来应用插件

    $ mkdir test-plugin
    

    顶级build.gradle加入一行include ':test-plugin'
    test-plugin/build.gradle加入:apply plugin:'org.samples.greeting'
    执行:./gradlew hello

    $ ./gradlew hello
    Hello, World!
    

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容