Gradle 自定义插件

使用版本 5.6.2
插件被用来封装构建逻辑和一些通用配置。将可重复使用的构建逻辑和默认约定封装到插件里,以便于其他项目使用。
你可以使用你喜欢的语言开发插件,但是最终是要编译成字节码在 JVM 运行的。
Gradle 有两种插件,脚本插件和二进制插件。
关于插件的介绍,可以参考我的另一篇文章 Gradle 插件
这里讲的自定义插件是二进制插件,二进制插件可以打包发布,有利于分享。
可以在三个地方定义插件
- 在脚本里
- 在 buildSrc 下
- 在单独的项目里
三个地方的插件的用途目的不同。
在脚本里的插件
其他项目无法使用,只能在本脚本里使用。
在 buildSrc 下
在项目的 buildSrc 目录下的插件,这个项目里的所有(子)项目都可以使用。
在单独的项目里
你可以为你的插件创建一个项目,这个项目可以打包发布 JAR,提供给其他任何项目使用。
创建一个插件
建议使用静态语言,例如 Java ,Kotlin,开发工具建议使用 IntelliJ IDEA 。
一个插件就是个实现了 Plugin 的类。
当插件被应用到项目时,Gradle 会实例化这个插件并调用 Plugin.apply() 方法,并将这个项目的实例当做参数传递进去。插件就可以对这个项目进行各种配置了。
CustomPLugin.java
// 定义一个插件
class CustomPLugin implements Plugin<Project>{
    @Override
    void apply(Project target) {
        // do something
    }
}
前面说到可以在三个地方创建插件,现在来一一实现下。
在脚本里创建一个插件
可以在 build.gradle 脚本里任意地方定义。
build.gradle
// 定义一个插件
class CustomPLugin implements Plugin<Project>{
    @Override
    void apply(Project target) {
      //添加一个任务
     target.task('hello', group: 'util') {
         doLast {
             logger.quiet("Hello Plugin.")
         }
     }
    }
}
//直接在脚本里应用
apply plugin:CustomPLugin
在 gradle 窗口就可以看到应用插件后的添加的任务

双击任务或者命令行输入都可以执行 hello 任务
gradle hello
在项目的 buildSrc 目录下创建项目
这里使用的是 Groovy 。
在这个目录下创建项目会被 Gradle 自动识别的。
结构如下

- 在项目根目录下创建目录 buildSrc
- 在 buildSrc 下按照 java 工程或者 groovy 工程(这取决于你用什么语言)新建目录
$projectDir/buildSrc/src/main/groovy
- 在 groovy 创建你的包 (可能现在还不能被识别为项目,那就创建目录),例如 com.github.skymxc
- 在包里创建插件,也就是创建一个实现了 Plugin 的类。
这里做简单的示范:
在插件里为 jar 任务添加一个操作:生成记录文件
JarLogPlugin.groovy
/**
 * 输出 生成记录到指定文件
 */
class JarLogPlugin implements Plugin<Project> {
    @Override
    void apply(Project target) {
        //增加一个扩展配置用来接收参数
        target.extensions.create("log", LogExtension)
        //添加一个任务
        target.task(type: Jar,group:'util','jarWithLog',{
            doLast {
                //使用配置
                def file = target.log.outputPath;
                if (file==null){
                    file = new File(target.projectDir,"/log/jarlog.txt").getPath()
                }
                println "存储目录是 ${file}"
                def content = "${getArchiveFileName().get()}---${getNow()}\n"
                writeFile(file,content)
            }
        })
        //为 jar 任务添加一个 操作,
        target.tasks.jar.doLast {
            println "当前时间是 ${getNow()},打了一个 jar-> ${version}"
            //存到指定文件记录
            def file = new File(target.projectDir,"/log/jarlog.txt");
            def content = "${version}---${getNow()}\n"
            writeFile(file.getAbsolutePath(),content)
        }
    }
    def String getNow(){
        def dateFormat = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss.SSS");
        return dateFormat.format(Calendar.getInstance().getTime());
    }
    def void writeFile(String path,String content){
        def file = new File(path);
        if (!file.exists()){
            if (!file.getParentFile().exists()){
                file.getParentFile().mkdirs();
            }
            file.createNewFile();
        }
        FileWriter writer = new FileWriter(file.getAbsolutePath(),true);
        BufferedWriter bufferedWriter = new BufferedWriter(writer);
        bufferedWriter.write(content);
        bufferedWriter.close();
    }
}
配置 DSL
上面使用了一个扩展来接收参数, 普通的对象就可以,例如
LogExtension.groovy
class LogExtension {
    String outputPath;
}
扩展在这里就是用来为插件配置 DSL 用的。
//为 项目添加了一个 LogExtension 类型的属性 名字是 log
project.extensions.create("log", LogExtension)
插件可以使用 DSL 接收参数,在插件或者任务里直接通过 Project 实例访问即可。
def file = project.log.outputPath;
插件创建完成后,在项目的里就可以使用了。
现在可以使用类名应用插件了。
build.gradle
import com.github.skymxc.JarLogPlugin
apply plugin: JarLogPlugin
插件应用成功后就可以使用 DSL 为插件配置参数。
配置记录文件地址:
build.gradle
log {
    outputPath rootProject.projectDir.getPath()+"\\record\\jar.txt"
}
为插件创建 ID
- 在 main 目录下创建 resources 文件夹
- 在 resources 目录下创建 META-INF 文件夹
- 在 META-INF 目录下创建 gradle-plugins 文件夹
- 在 gradle-plugins 目录下创建 properties 文件,名字就是你的插件 ID。
- 在 id.properties 文件里通过 implementation-class 指向你的实现类。
例如
src / main / resources / META-INF / gradle-plugins / com.github.skymxc.sample.properties
implementation-class= com.github.skymxc.JarLogPlugin
然后就可以使用插件 ID 了
plugins {
    id 'com.github.skymxc.sample'
}
关于插件 id 的规范:
- 可以包含任何字母数字字符 “ . ”和 “ - ”。
- 必须至少包含一个 “ . ” 。
- 一般使用小写的反向域名。(类似包名)
- 不能以 “ . ” 结尾。
- 不能包含连续的 “ . ” 。
关于 Groovy 的语法,可以参考 Groovy 语法。
在单独的项目里创建插件
这次仍然是使用 Groovy 语言。
这里的插件项目其实就是一个 Groovy 项目,当然了你如果使用 Java 语言就是一个 Java 工程。
创建一个工程


创建出来的项目就是这样子,标准的 Groovy 工程目录

更改 build.gradle 脚本,配置项目
- 应用 maven-publih 插件
- 添加 Gradle 和 Groovy 的依赖
- 配置上传任务
最后就是这样子
plugins {
    id 'groovy'
    id 'maven-publish'
}
group 'com.github.skymxc'
version '1.0.0'
sourceCompatibility = 1.8
repositories {
    mavenCentral()
}
//使用 groovy 和 gradle 依赖
dependencies {
    compile gradleApi()
    compile localGroovy()
}
publishing {
    repositories {
        maven {
            name 'local'
            url 'file://E:/libs/localMaven'
        }
    }
    publications {
        maven(MavenPublication) {
            groupId = 'com.github.skymxc'
            artifactId = 'plugin'
            version = '1.0.0'
            from components.java
        }
    }
}
创建两个插件:
一个是上面创建的那个,就不重复粘贴了。
另一个插件 Greet,配置一个任务,简单的输出一句话。
class Greet implements Plugin<Project> {
    @Override
    void apply(Project target) {
        target.extensions.create("hello", Hello)
        target.task("hello") {
            doLast {
                println "message -> ${target.hello.message}"
            }
        }
    }
}
Hello.groovy
class Hello {
    String message
}
插件 ID 的配置是跟上面一样的。

执行 maven-publish 的 publish 任务,将插件发布到指定仓库。

发布成功后的仓库

插件创建完成了,也发布了,下面就是使用这个插件了。
这里对插件的使用就简单介绍一下,详细的可以查看之前的这篇介绍:Gradle 插件
- 在根项目的 build.gradle 配置仓库,添加依赖
buildscript {
    repositories {
        maven {
            url 'file://E:/libs/localMaven'
        }
    }
    dependencies {
        classpath 'com.github.skymxc:plugin:1.0.2'
    }
}
- 应用插件
我分别在两个 Java 项目里使用了插件:
- 一个是使用 id 的方式
- 一个是使用类名的方式
lib_2/ build.gradle 使用 类名的方式
······
apply plugin:'com.github.skymxc.greet'
hello{
    message '使用了 com.github.skymxc.greet 插件'
}
······
lib_1/ build.gradle 使用 id 的方式
plugins {
    id 'java'
    id 'com.github.skymxc.jarlog'
}
······
logConfigure {
    outputPath rootProject.projectDir.getPath()+"\\record\\jar.txt"
}
应用插件后的 gradle 视图,可以看到已经添加的任务。

使用 java-gradle-plugin 开发插件
像上面一样创建一个项目,不过这次是一个 java 项目,然后应用这个插件。
java-gradle-plugin 可以减少重复代码,它自动的应用 java 插件,添加 gradleApi() 依赖。
plugins {
    id 'java-gradle-plugin'
}
使用 gradlePlugin {} 配置块可以配置开发的每一个插件,不用手动创建对应的属性文件了。
gradlePlugin {
    plugins {
        greetPlugin {
            id = 'com.github.skymxc.greet'
            implementationClass = 'com.github.skymxc.GreetPlugin'
        }
        jarWithLogPlugin {
            id = 'com.github.skymxc.jar-log'
            implementationClass = 'com.github.skymxc.JarWithLogPlugin'
        }
    }
}
插件会在 jar 文件里自动生成对应的 META-INF 目录。
配合 maven-publish 可以为每个插件创建对应的发布任务。
在发布时也会为每个插件发布对应的 “插件标记工件” 。
插件标记工件
关于 插件标记工件这里插一下:
每个 maven 工件都是由三部分标识的
- groupId
- artifactId
- version
平常我们添加依赖的这样的:
implementation 'groupId:artifactId:version'
而我们的插件是通过 id 应用的,怎么通过 id 找到对应的工件呢,这就有了“插件标记工件”。
应用插件时会把 id 映射成这样:plugin.id: plugin.id.gradle.plugin:plugin.version
即:
- plugin.id
- plugin.id.gradle.plugin
- plugin.version
举个上面的例子:com.github.skymxc.greet 插件对应的工件就是:
com.github.skymxc.greet:com.github.skymxc.greet.gradle.plugin:1.0.0
部分代码:
plugins {
    id 'java-gradle-plugin'
    id 'maven-publish'
}
group 'com.github.skymxc'
version '1.0.0'
gradlePlugin {
    plugins {
        greetPlugin {
            id = 'com.github.skymxc.greet'
            implementationClass = 'com.github.skymxc.GreetPlugin'
        }
        jarWithLogPlugin {
            id = 'com.github.skymxc.jar-log'
            implementationClass = 'com.github.skymxc.JarWithLogPlugin'
        }
    }
}
publishing {
    repositories {
        maven {
            name 'local'
            url 'file://E:/libs/localMaven'
        }
    }
}
maven-publish 的任务
简单介绍一下 maven-publish 的发布任务
- generatePomFileFor - ${PubName}Publication- 为名字为 PubName 的的发布创建一个 POM 文件,填充已知的元数据,例如项目名称,项目版本和依赖项。POM文件的默认位置是build / publications / $ pubName / pom-default.xml。 
- publish - ${PubName}PublicationTo- ${RepoName}Repository- 将 PubName 发布 发布到名为 RepoName 的仓库。 
 如果仓库定义没有明确的名称,则 RepoName 默认为 “ Maven”。
- publish - ${PubName}PublicationToMavenLocal- 将 PubName 发布以及本地发布的 POM 文件和其他元数据复制到本地Maven缓存中 
 (通常为$USER_HOME / .m2 / repository)。
- publish - 依赖于:所有的 publish - ${PubName}PublicationTo- ${RepoName}Repository 任务
 将所有定义的发布发布到所有定义的仓库的聚合任务。不包括复制到本地 Maven 缓存的任务。
- publishToMavenLocal - 依赖于:所有的 publish - ${PubName}PublicationToMavenLocal 任务- 将所有定义的发布(包括它们的元数据(POM文件等))复制到本地Maven缓存。 
这张图列出了为每个插件生成的对应的任务。

执行发布任务 publish 后可以在对应的仓库查看


发布插件后的使用
- 配置仓库,这次在 settings.gradle 里配置
pluginManagement {
    repositories {
        maven {
            url 'file://E:/libs/localMaven'
        }
    }
}
- 使用插件
plugins {
    id 'java'
    id 'com.github.skymxc.greet' version '1.0.13'
    id 'com.github.skymxc.jar-log' version '1.0.0'
}
应用插件后就可以在 Gradle 的窗口看到对应的任务了。
然后可以根据需要配置 DSL 。
为插件配置 DSL
和插件的交互就是通过 DSL 配置块进行的。
那怎么为插件配置 DSL 呢,答案是随便一个普通类都可以的。
通过 Gradle 的 API 可以将一个普通的类添加为 Project 的扩展,即 Project 的属性。
举个例子,插件里的任务需要两个参数:文件地址,文件名字,就要通过 DSL 配置的方式解决。
JarLogExtension.java 一个普通的类,有两个属性,分别是 name , path
package com.github.skymxc.extension;
public class JarLogExtension {
    private String name;
    private String path;
    //省略 setter/getter
}
在插件里将这个类添加为项目的扩展。
public class JarWithLogPlugin implements Plugin<Project> {
    @Override
    public void apply(Project target) {
        //添加扩展
        target.getExtensions().add("jarLog", JarLogExtension.class);
        //创建任务
        target.getTasks().create("jarWithLog", JarWithLogTask.class);
    }
}
应用插件后就可以在脚本里使用这个 DSL 配置了。
build.gradle
······
/**
 * 为 jarWithLog 配置的 DSL
 */
jarLog {
    path getBuildDir().path+"/libs"
    name "record.txt"
}
······
接下来就是在插件或者任务里获取 DSL 配置的参数了。
可以通过名字或者类型获取到这个扩展对象。
public class JarWithLogTask extends Jar {
    @TaskAction
    private void writeLog() throws IOException {
      //获取到配置
        JarLogExtension extension = getProject().getExtensions().getByType(JarLogExtension.class);
        File file = new File(extension.getPath(),extension.getName());
        String s = file.getAbsolutePath();
        String content = getNow()+" --- "+getArchiveFileName().get();
        System.out.println("path --> "+s);
        writeFile(s,content);
    }
}
嵌套 DSL
在我们日常的使用中,嵌套 DSL 很常见,那怎么实现的呢。
hello {
    message '使用 pluginManagement 管理插件'
    user {
        name 'mxc'
        age 18
    }
}
现在我来实现下:
首先是创建里面的嵌套对象,需要注意的是要为 DSL 配置对应的方法。
UserData.java
package com.github.skymxc.extension;
/**
 * 为了实践嵌套 DSL 建的
 */
public class UserData {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    /**
     * 注意此方法 没有 set
     * @param name
     */
    public void name(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void age(int age) {
        this.age = age;
    }
}
然后是外层的 DSL 对应的类,因为有 DSL 嵌套,所以要使用闭包
package com.github.skymxc.extension;
import org.gradle.api.Action;
/**
 * 为 HelloTask 创建的扩展,用于接收配置参数
 */
public class HelloExtension {
    private String message;
    private final UserData user = new UserData();
    /**
     * 注意此方法没有 set
     * @param action
     */
    public void user(Action<? super UserData> action) {
        action.execute(user);
    }
    // 省略其他 getter/setter
}
最后就是添加到项目的扩展了,和前面一样
public class GreetPlugin implements Plugin<Project> {
    @Override
    public void apply(Project target) {
        target.getExtensions().create("hello", HelloExtension.class);
        target.getTasks().create("hello", HelloTask.class);
    }
}
在任务中的获取也是一样的
HelloExtension hello = getProject().getExtensions().getByType(HelloExtension.class);
UserData user = hello.getUser();
集合对象
再看一个 DSL 配置,这种集合嵌套也经常见到,下面也来简单实现一下。
fruits {
    apple {
        color '红色'
    }
    grape {
        color '紫红色'
    }
    banana {
        color '黄色'
    }
    orange {
        color '屎黄色'
    }
}
这种配置是配合 NamedDomainObjectContainer 实现的,它接收一个类,这个类必须有一个包含 name 参数的构造方法。
Fruit.java
/**
 * 必须有一个 name 属性,并且有一个 name 参数的构造函数
 */
public class Fruit {
    private String name;
    private String color;
    public Fruit(String name) {
        this.name = name;
    }
    public void color(String color){
        setColor(color);
    }
    //省略 setter/getter
}
配置一个 Factory
FruitFactory.java
import org.gradle.api.NamedDomainObjectFactory;
import org.gradle.internal.reflect.Instantiator;
public class FruitFactory implements NamedDomainObjectFactory<Fruit> {
    private Instantiator instantiator;
    public FruitFactory(Instantiator instantiator) {
        this.instantiator = instantiator;
    }
    @Override
    public Fruit create(String name) {
        return instantiator.newInstance(Fruit.class, name);
    }
}
接着就是创建 NamedDomainObjectContainer 对象并添加到 Project 。
GreetPlugin.java
public class GreetPlugin implements Plugin<Project> {
    @Override
    public void apply(Project target) {
        Instantiator instantiator = ((DefaultGradle)target.getGradle()).getServices().get(Instantiator.class);
        NamedDomainObjectContainer<Fruit> fruits = target.container(Fruit.class,new FruitFactory(instantiator));
        target.getExtensions().add("fruits",fruits);
        target.getTasks().create("printlnFruits", ShowFruitTask.class);
    }
}
现在应用这个插件就可以在脚本里使用上述的 DSL 配置了。
最后是 DSL 配置的接收了
public class ShowFruitTask extends DefaultTask {
    @TaskAction
    public void show(){
        NamedDomainObjectContainer<Fruit> fruits = (NamedDomainObjectContainer<Fruit>) getProject().getExtensions().getByName("fruits");
        fruits.forEach(fruit -> {
            String format = String.format("name: %s , color: %s", fruit.getName(), fruit.getColor());
            getLogger().quiet("fruit : {}",format);
        });
    }
}
关于自定义插件的相关介绍就这些了,更详细的文档可以查看 Gradle 用户手册
这篇文章的源码已经放在 github 上:GradlePractice
资料
- 自定义插件 https://docs.gradle.org/current/userguide/custom_plugins.html
- 开发辅助插件 https://docs.gradle.org/current/userguide/java_gradle_plugin.html
- 使用插件 https://docs.gradle.org/current/userguide/plugins.html
- 发布 https://docs.gradle.org/current/userguide/publishing_overview.html
- maven 发布插件 https://docs.gradle.org/current/userguide/publishing_maven.html
- Gradle 教程 https://gradle.org/guides/?q=Plugin Development
- Gradle DSL https://blog.csdn.net/zlcjssds/article/details/79229209
End
Gradle 自定义插件的更多相关文章
- Android gradle 自定义插件
		Gradle 的插件有三种打包方式: 构建脚本:插件逻辑写在 build.gradle 中,适用于逻辑简单的任务,但是该方式实现的插件在该构建脚本之外是不可见的,只能用于当前脚本. buildSrc项 ... 
- gradle  自定义插件  下载配置文件
		1.新建Gradle项目: 2.建立src/main/groovy目录,并添加如下代码: ConfigPlugin.groovy package com.wemall.config import or ... 
- AS 自定义 Gradle plugin 插件 案例 MD
		Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ... 
- ionic2/cordova自定义插件集成aar包
		一.准备自定义插件 1. 准备:安装plugman npm install -g plugman 2. 新建组件 plugman create --name MyPlugin --plugin_id ... 
- gradle ssh 插件
		org.hidetake.ssh Gradle SSH Plugin is a Gradle plugin which provides remote command execution and fi ... 
- 深入学习jQuery自定义插件
		原文地址:jQuery自定义插件学习 1.定义插件的方法 对象级别的插件扩展,即为jQuery类的实例增加方法, 调用:$(选择器).函数名(参数); $(‘#id’).myPlugin(o ... 
- jquery自定义插件结合baiduTemplate.js实现异步刷新(附源码)
		上一篇记录了BaiduTemplate模板引擎使用示例附源码,在此基础上对使用方法进行了封装 自定义插件jajaxrefresh.js 代码如下: //闭包限定命名空间 (function ($) { ... 
- [原创]使用Gradle的插件EnIDE build的时候,输出的中文总是乱码。
		使用Gradle的插件EnIDE build的时候,输出的中文总是乱码.解决办法:在EnIDE的设置里面,设置JVM options GRADLE_OPTS 为:-Dfile.encoding=UTF ... 
- Phonegap中自定义插件的使用
		在phonegap中需要实现特定相关的功能,可能需要自定义扩展一下功能,那么扩展phonegap组件就成为了可能. 源代码结构图: 本文目的在于讲述怎么扩展一个phonegap组件以及实现. 针对ph ... 
随机推荐
- Rust 入门 (三)_下
			这部分我们学习 rust 语言的 变量.数据类型.函数.注释.流程控制 这五个方面的内容.前文介绍了前两个内容,本文介绍后三个内容. 函数 函数在 rust 代码普遍存在,我们也已经见过了它的主函数 ... 
- [ch04-03] 用神经网络解决线性回归问题
			系列博客,原文在笔者所维护的github上:https://aka.ms/beginnerAI, 点击star加星不要吝啬,星越多笔者越努力. 4.3 神经网络法 在梯度下降法中,我们简单讲述了一下神 ... 
- 大白话OSI七层协议
			目录 一.物理层 二.数据链路层 2.1 以太网协议 2.2 Mac地址 2.3 广播地址 三.网络层 3.1 IP协议详解 3.1.1 IP地址的两部分 3.2 子网掩码详解 3.3 IP数据包详解 ... 
- 【SSL1457】翻币问题
			题面: \[\Large\text{翻币问题}\] \[Time~Limit:1000MS~~Memory~Limit:65536K\] Description 有N个硬币(6<=N<=2 ... 
- .Neter所应该彻底了解的委托
			本文将通过引出几个问题来,并且通过例子来剖析C#中的委托以及用法,做抛砖引玉的作用 对于委托我发现大部分人都有以下问题,或者可能在面试中遇过这样的: 委托是不是相当于C/C++的函数指针? 委托究竟是 ... 
- 源码分析—ThreadPoolExecutor线程池三大问题及改进方案
			前言 在一次聚会中,我和一个腾讯大佬聊起了池化技术,提及到java的线程池实现问题,我说这个我懂啊,然后巴拉巴拉说了一大堆,然后腾讯大佬问我说,那你知道线程池有什么缺陷吗?我顿时哑口无言,甘拜下风,所 ... 
- HDU3191-How many paths are there(次短路的长度及其个数)
			oooccc1 is a Software Engineer who has to ride to the work place every Monday through Friday. For a ... 
- ssh远程管理服务的介绍
			第6章 远程管理的介绍 6.1 服务的概念介绍 6.1.1 ssh和telnet服务的相同和不同点 ssh: 服务端口号为22 在数据传输的时候是加密的传输 一般在互联网中使用,可以使用root账号进 ... 
- 【CuteJavaScript】Angular6入门项目(4.改造组件和添加HTTP服务)
			本文目录 一.项目起步 二.编写路由组件 三.编写页面组件 1.编写单一组件 2.模拟数据 3.编写主从组件 四.编写服务 1.为什么需要服务 2.编写服务 五.引入RxJS 1.关于RxJS 2.引 ... 
- Java中真的只有值传递么?
			Java中真的只有值传递么? (本文非引战或diss,只是说出自己的理解,欢迎摆正心态观看或探讨) 回顾值传递和引用传递 关于Java是值传递还是引用传递,网上有不一样的说法. 1.基本类型或基本类型 ... 
