自定义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 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文件的属性将包含从以下项目属性派生的标识值:
-
groupId - Project.getGroup()
-
artifactId - Project.getName()
-
version - Project.getVersion()
配置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'
}
}
我测试了一遍,但是构建过程报错如下图,当前还未找到解决方法。如果有同学碰到该问题,希望在评论中给出一点指点意见。
报错