自定义Gradle Plugin 知识点总结

私以为,要学习自定义Gradle插件,首先要搞清楚自定义Plugin的意义。或者说,自定义Plugin有什么作用?只有弄清楚这个问题,再去说怎么用Gradle Plugin才是有意义的,这里引用官方文档的一段话。

A Gradle plugin packages up reusable pieces of build logic, which can be used across many different projects and builds.

简略翻译一下,Gradle插件打包可重用的构建逻辑,(这些构建逻辑)可用在很多不同的项目及构建中。

这里,我还想说说“构建”这一概念,我们经常听到这个词,但真的明白什么是构建吗?构建到底是做什么?这里我也直接引用阮一峰在讲make命令的一篇博文中一小段来阐明何为“构建”。

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。
原文地址:http://www.ruanyifeng.com/blog/2015/02/make.html

通常,Gradle插件根据定义位置的不同,可分三种情形:

  • Build script
    在build.gradle文件中直接编写plugin的代码,好处就是plugin会自动被编译,同时自动被包括到build script的classpath中。弊端是对外部的build script不可见,即不可重用。
  • buildSrc 模块
    虽然官方文档中使用了 buildSrc project这一个词,但buildSrc project并不是真的一个独立的项目,buildSrc还是定义在原来项目中,只不过buildSrc目录与src目录同级别。
  • Standalone project(单独的项目)
    毫无疑问,这里的project就是一个单独的项目了。这个项目生成并发布一个jar包,该jar包可在多个构建中使用。
具体定义
package org.gradle.api;

public interface Plugin<T> {
    void apply(T var1);
}

要自定义Gradle插件,需要实现Plugin接口,重写apply(T var1)方法。对apply方法源码感兴趣,可查看我的另一篇文章《apply plugin: 'xxx'到底做了啥》。从上面的代码中,看到apply方法参数是泛型参数。具体参数类型取决于script类别,在build script中使用,apply的类型参数为Project;在settings script中使用,类型参数为Settings;在init script中使用,类型参数为Gradle。

插件配置

多数插件都会在build script中获取一些配置参数。这里涉及到extension类型的对象。Project对象与一个ExtensionContainer对象关联,该ExtensionContainer对象包含所有插件应用到项目时可能需要的设置和属性。extension类其实就是一个简单的、自定义的Java Bean类,其中的属性就是应用插件时需要配置的属性。如下,

public class DemoExtension {

    public 基本数据类型或String 属性名1;
    public 基本数据类型或String 属性名2;
   
    //相应的getter和setter
}

这里说明一下,Extension类中的属性类型可以是Groovy内置的基本数据类型或者String类。当然,也可以是复合类型(自定义类)。这里先以内置类型String为例说明。复合类型在文章后面提到。如下,是一段比较常见的示例代码,

class GreetingPluginExtension {
    String message
    String greeter
}

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

apply plugin: GreetingPlugin

// Configure the extension using a DSL block
greeting {
    message = 'Hi'
    greeter = 'Gradle'
}
自定义task和plugin中文件操作

自定义task、plugin难免会涉及到文件操作。在开发自定义任务或插件时,最好在接受文件位置的输入配置时保持灵活性。为此,可以利用Project.file(java.lang.Object)方法延迟将值解析为文件。

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) {
    doLast {
        println file(greetingFile).text
    }
}

ext.greetingFile = "$buildDir/hello.txt"
单独的项目

单独项目中定义自定义插件,可以将其发布到远程仓库或者本地仓库,这样其他的项目也可以复用该插件。

具体的步骤不展开说了,网上的博客都比较详细。这里讲一下plugin id。

Plugin id

先说明,plugin id是指项目中 resources/META-INF.gradle-plugins/xxx.properties properties文件的文件名。这个文件名是什么,plugin id就是什么。
plugin id与properties文件名保持一致

在build script中引用插件时的写法。

插件id的名称与java包的类似,都是全限定名(fully qualified),这样命名一方面避免名字冲突,一方面为分组提供便利(参照Java)。
插件id应该是由两部分组成:一部分能反映作者或者作者归属的组织;一部分是插件本身名称,规范命名:com.组织名称.插件名称。例如,你有一个名称为“foo”的Github账号,你的插件名称为“bar”,那么一个比较规范的插件id为com.github.foo.bar。

插件id命名应该遵从以下规范:

  • 可以包含任何字母数字字符、(.)、(-);
  • 必须包含至少一个(.)字符分隔命名空间和插件名称;
  • 命名空间通常使用小写反向域名命名,例如com.xxx、 org.xxx;
  • 插件名称通常只用小写字符;
  • 不能以(.)开头或结尾;
  • 不能包含连续的(.),比如(..)。

NOTE:避免"gradle"定义在插件id中。因为插件id本身只用在Gradle插件中,其中再出现"gradle"是多余的。

发布插件

如果想要将插件在团队内部或者公开使用,可以发布插件到远程仓库,远程仓库有Ivy 、 Maven。Gradle文档中有相应的内容(Maven Publish PluginIvy Publish Plugin)。Maven Publish Plugin和Ivy Publish Plugin的内容结构一致,以下是文档结构对比,

Maven publish plugin结构

Ivy publish plugin结构

本文就以Maven Publish为例,浅谈一下远程发布。对Ivy发布有兴趣的同学可自行对照相关Gradle文档结构查看。

发布到Maven

首先添加Maven Publish Plugin引用,

plugins {
    id 'maven-publish'
}

Maven Publish Plugin在项目中使用一个名为publishing的扩展(extension),这个extension的类型为PublishingExtension。在extension中有两个container,一个为publications,一个为repositories。Maven Publish Plugin同MavenPublication类型的publications和MavenArtifactRepository类型的repositories一起发挥作用。

publishing {
    publications {
       ...
    }

    repositories{
      ...
    }
}

相关任务

  • generatePomFileForPubNamePublication
    生成一个名为PubName的POM文件,文件内容包括在build script声明好的项目名称,项目版本,项目依赖。POM文件默认位置在build/publications/$pubName/pom-default.xml。
  • publishPubNamePublicationToRepoNameRepository
    将名为PubName的publication发布到名为RepoName的库,PubName默认为"Maven"。
  • publishPubNamePublicationToMavenLocal
    将名为PubName的publication复制到本地Maven缓存(路径通常为$USER_HOME/.m2/repository)。包括发布的POM文件和其他元数据。
  • publish
    依赖所有的publishPubNamePublicationToRepoNameRepository任务。
    该任务将所有已定义的publication发布到所有已定义的库。但不包括将publications复制到本地Maven缓存。
  • publishToMavenLocal
    依赖所有的publishPubNamePublicationToMavenLocal任务。将所有已定义的publications复制到本地Maven缓存,包括其中的元数据(POM文件等)。
    Maven相关任务

Publications
maven publish 插件提供了MavenPublication类型的publications块。Maven publication中有四个主要的配置项:

  • component —— 通过MavenPublication.from(org.gradle.api.component.SoftwareComponent)配置。当前支持3种组件:'components.java' (added by the JavaPlugin), 'components.web' (added by the WarPlugin) and 'components.javaPlatform' (added by the JavaPlatformPlugin).
  • 自定义 artifacts —— 调用MavenPublication.artifact(java.lang.Object)方法配置。
  • 标准元数据 —— 包括artifactId, groupId, version。
  • POM文件的其他内容。—— 调用MavenPublication.pom(org.gradle.api.Action)配置。

生成POM文件中的识别值
生成的POM文件的属性将包含从以下项目属性派生的标识值:

配置MavenPublication时,直接指定groupId, artifactId, version即可覆盖这些属性的默认值。

 publishing {
        publications {
            maven(MavenPublication) {
                groupId = 'org.gradle.sample'
                artifactId = 'project1-sample'
                version = '1.1'

                from components.java
            }
        }
    }

自定义生成POM文件内容
发布插件时,Maven Publish插件提供了一个DSL来设置一些元数据。以下的示例代码展示了一些常用的属性设置。

publishing {
    publications {
        mavenJava(MavenPublication) {
            pom {
                name = 'My Library'
                description = 'A concise description of my library'
                url = 'http://www.example.com/library'
                properties = [
                    myProp: "value",
                    "prop.with.dots": "anotherValue"
                ]
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0'
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id = 'johnd'
                        name = 'John Doe'
                        email = 'john.doe@example.com'
                    }
                }
                scm {
                    connection = 'scm:git:git://example.com/my-library.git'
                    developerConnection = 'scm:git:ssh://example.com/my-library.git'
                    url = 'http://example.com/my-library/'
                }
            }
        }
    }
}

自定义插件版本(略)
该部分截至文章发布时,在实际项目中还没有使用过,所以,这部分先省略,感兴趣的同学可以点击链接,自行查看这部分内容,内容不多。

Repositories

Repositories中有两个主要的配置项:

  • URL(必需)
  • Name(可选)

示例代码如下:

publishing {
    publications {
       ...
    } 
    repositories {
        maven {
            url = "$buildDir/repo"
        }
    }
}

这里提一下,URL经常用到,那么Name用来做什么?Name用来区分不同的repositories,不同Name的repositories可以同时定义在build script中。如果只有一个repositories时,不指定Name,那么该repositories的Name隐式指定为"Maven"。

Snapshot版repositories & release版repositories
将snapshot版和release版的repositories发布到不同的Maven库是比较常见的做法。直接基于项目version配置存储库URL。如下示例代码,

publishing {
    repositories {
        maven {
            def releasesRepoUrl = "$buildDir/repos/releases"
            def snapshotsRepoUrl = "$buildDir/repos/snapshots"
            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl
        }
    }
}

或者,在gradle.properties文件中定义一个键为'release'的系统属性,通过判断project是否包含该属性来决定具体的发布URL。

publishing {
    repositories {
        maven {
            def releasesRepoUrl = "$buildDir/repos/releases"
            def snapshotsRepoUrl = "$buildDir/repos/snapshots"
            url = project.hasProperty('release') ? releasesRepoUrl : snapshotsRepoUrl
        }
    }
}

Publishing && uploadArchives区别
在Android项目中的build script中我们经常会见到uploadArchives task发布插件到库的代码块,如下所示,

uploadArchives {
    repositories {
        mavenDeployer {
            //设置插件的GAV参数
            pom.groupId = publishedGroupId
            pom.version = libraryVersion
            pom.artifactId = artifactId
            //文件发布到下面目录
            repository(url: uri('../repo'))
        }
    }
}

这里不免产生疑问:前面提到的publishing和uploadArchives貌似都是发布插件到库,那两者之间的区别是什么?为什么会存在两种方式的发布

通过查询文档,找到了一些蛛丝马迹。这篇文档中提到,uploadArchives是Gradle1.0时,提供的初始发布机制,基于Upload tasks。文中也说到,uploadArchives随后会被一种选择模型代替,这里的选择模型就是指publishing,而且文中明确提到uploadArchives不应在新版本中使用。但是没提新版本指哪个版本号开始。至于现在为什么还能使用,这当然是一种向后兼容的方式,确保当前使用不会出问题。这里引用一下原文,

This chapter describes the original publishing mechanism available in Gradle 1.0, which has since been superseded by an alternative model. The approach detailed in this chapter — based on Upload tasks — should not be used in new builds. We cover it in order to help users work with and update existing builds that use it.

同时,在Stack Overflow中的相关问题的一个回答对该问题有所补充。

具体问题

两种发布方式的差异还在于,uploadArchives附带java plugin,puhlishing则与java plugin分离,分为maven和ivy。

为插件提供可配置的DSL

前面我们有使用extension类型的对象为插件提供配置。extension类型的对象继承Gradle DSL,可以为插件添加项目属性及DSL块。

内嵌DSL元素

要创建嵌套的DSL元素,需要使用ObjectFactory类型创建类似装饰的对象。然后,可以通过插件扩展的属性和方法使这些装饰对象对DSL可见:

class Person {
    String name
}

class GreetingPluginExtension {
    String message
    final Person greeter

    @javax.inject.Inject
    GreetingPluginExtension(ObjectFactory objectFactory) {
        // Create a Person instance
        greeter = objectFactory.newInstance(Person)
    }

    void greeter(Action<? super Person> action) {
        action.execute(greeter)
    }
}

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        // Create the 'greeting' extension
        def extension = project.extensions.create('greeting', GreetingPluginExtension)
        project.task('hello') {
            doLast {
                println "${extension.message} from ${extension.greeter.name}"
            }
        }
    }
}

apply plugin: GreetingPlugin

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

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

推荐阅读更多精彩内容