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 目錄結構

  1. 在項目根目錄下創建目錄 buildSrc
  2. 在 buildSrc 下按照 java 工程或者 groovy 工程(這取決於你用什么語言)新建目錄

$projectDir/buildSrc/src/main/groovy

  1. 在 groovy 創建你的包 (可能現在還不能被識別為項目,那就創建目錄),例如 com.github.skymxc
  2. 在包里創建插件,也就是創建一個實現了 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

  1. 在 main 目錄下創建 resources 文件夾
  2. 在 resources 目錄下創建 META-INF 文件夾
  3. 在 META-INF 目錄下創建 gradle-plugins 文件夾
  4. 在 gradle-plugins 目錄下創建 properties 文件,名字就是你的插件 ID。
  5. 在 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 工程目錄

創建Groovy工程

更改 build.gradle 腳本,配置項目

  1. 應用 maven-publih 插件
  2. 添加 Gradle 和 Groovy 的依賴
  3. 配置上傳任務

最后就是這樣子

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 任務,將插件發布到指定倉庫。

gradlew -p plugin publish

發布成功后的倉庫

發布成功的圖片

插件創建完成了,也發布了,下面就是使用這個插件了。

這里對插件的使用就簡單介紹一下,詳細的可以查看之前的這篇介紹:Gradle 插件

  1. 在根項目的 build.gradle 配置倉庫,添加依賴
buildscript {
    repositories {
        maven {
            url 'file://E:/libs/localMaven'
        }
    }
    dependencies {
        classpath 'com.github.skymxc:plugin:1.0.2'
    }
}
  1. 應用插件

我分別在兩個 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 視圖,可以看到已經添加的任務。

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 后可以在對應的倉庫查看

發布后的倉庫圖1
發布后的倉庫圖2

發布插件后的使用

  1. 配置倉庫,這次在 settings.gradle 里配置
pluginManagement {
    repositories {
        maven {
            url 'file://E:/libs/localMaven'
        }
    }
}
  1. 使用插件
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

資料

End


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM