二進制外掛指的是編譯並以 JAR 檔案形式發佈的外掛。這些外掛通常以 Java 或 Kotlin 撰寫,並為 Gradle 建置提供自訂功能或任務。

使用外掛開發外掛

Gradle 外掛開發外掛可用於協助開發 Gradle 外掛。

此外掛將自動套用 Java 外掛、將 gradleApi() 相依性新增至 api 配置、在產生的 JAR 檔案中產生所需的外掛描述符,並配置 外掛標記成品 以供發佈時使用。

若要套用並配置此外掛,請將以下程式碼新增至您的建置檔

build.gradle.kts
plugins {
    `java-gradle-plugin`
}

gradlePlugin {
    plugins {
        create("simplePlugin") {
            id = "org.example.greeting"
            implementationClass = "org.example.GreetingPlugin"
        }
    }
}
build.gradle
plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        simplePlugin {
            id = 'org.example.greeting'
            implementationClass = 'org.example.GreetingPlugin'
        }
    }
}

建議在開發外掛時撰寫和使用自訂任務類型,因為這會自動受益於增量建置。將此外掛套用至您的專案還有一個額外的好處,即 validatePlugins 任務會自動檢查自訂任務類型實作中定義的每個公用屬性是否存在輸入/輸出註解。

建立外掛 ID

外掛 ID 的目的是要全域唯一,類似於 Java 套件名稱(即,反向網域名稱)。此格式有助於防止命名衝突,並允許將具有相似所有權的外掛分組。

明確的外掛識別碼簡化了將外掛套用至專案的流程。您的外掛 ID 應結合反映命名空間(合理指向您或您的組織)和其提供的外掛名稱的組件。例如,如果您的 Github 帳戶名為 foo,而您的外掛名為 bar,則合適的外掛 ID 可能為 com.github.foo.bar。同樣地,如果外掛是在 baz 組織開發的,則外掛 ID 可能為 org.baz.bar

外掛 ID 應遵守以下準則

  • 可以包含任何英數字元、「.」和「-」。

  • 必須包含至少一個「.」字元,將命名空間與外掛名稱分隔開來。

  • 慣例上,命名空間使用小寫反向網域名稱慣例。

  • 慣例上,名稱中僅使用小寫字元。

  • 不得使用 org.gradlecom.gradlecom.gradleware 命名空間。

  • 不能以「.」字元開頭或結尾。

  • 不能包含連續的「.」字元(即「..」)。

對於外掛 ID 而言,識別所有權的命名空間和名稱就已足夠。

當在單個 JAR 成品中捆綁多個外掛時,建議遵循相同的命名慣例。這種做法有助於邏輯性地將相關外掛分組。

在單個專案中可以定義和註冊(透過不同的識別碼)的外掛數量沒有限制。

以類別形式撰寫的外掛識別碼應在包含外掛類別的專案建置腳本中定義。為此,需要套用 java-gradle-plugin

buildSrc/build.gradle.kts
plugins {
    id("java-gradle-plugin")
}

gradlePlugin {
    plugins {
        create("androidApplicationPlugin") {
            id = "com.android.application"
            implementationClass = "com.android.AndroidApplicationPlugin"
        }
        create("androidLibraryPlugin") {
            id = "com.android.library"
            implementationClass = "com.android.AndroidLibraryPlugin"
        }
    }
}
buildSrc/build.gradle
plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        androidApplicationPlugin {
            id = 'com.android.application'
            implementationClass = 'com.android.AndroidApplicationPlugin'
        }
        androidLibraryPlugin {
            id = 'com.android.library'
            implementationClass = 'com.android.AndroidLibraryPlugin'
        }
    }
}

使用檔案

在開發外掛時,最好在接受檔案位置的輸入配置時保持彈性。

建議使用 Gradle 的 受管理屬性project.layout 來選擇檔案或目錄位置。這將啟用延遲配置,以便僅在需要檔案時才解析實際位置,並且可以在建置配置期間隨時重新配置。

此 Gradle 建置檔定義了一個 GreetingToFileTask 任務,該任務將問候語寫入檔案。它還註冊了兩個任務:greet,用於建立包含問候語的檔案;以及 sayGreeting,用於列印檔案的內容。greetingFile 屬性用於指定問候語的檔案路徑

build.gradle.kts
abstract class GreetingToFileTask : DefaultTask() {

    @get:OutputFile
    abstract val destination: RegularFileProperty

    @TaskAction
    fun greet() {
        val file = destination.get().asFile
        file.parentFile.mkdirs()
        file.writeText("Hello!")
    }
}

val greetingFile = objects.fileProperty()

tasks.register<GreetingToFileTask>("greet") {
    destination = greetingFile
}

tasks.register("sayGreeting") {
    dependsOn("greet")
    val greetingFile = greetingFile
    doLast {
        val file = greetingFile.get().asFile
        println("${file.readText()} (file: ${file.name})")
    }
}

greetingFile = layout.buildDirectory.file("hello.txt")
build.gradle
abstract class GreetingToFileTask extends DefaultTask {

    @OutputFile
    abstract RegularFileProperty getDestination()

    @TaskAction
    def greet() {
        def file = getDestination().get().asFile
        file.parentFile.mkdirs()
        file.write 'Hello!'
    }
}

def greetingFile = objects.fileProperty()

tasks.register('greet', GreetingToFileTask) {
    destination = greetingFile
}

tasks.register('sayGreeting') {
    dependsOn greet
    doLast {
        def file = greetingFile.get().asFile
        println "${file.text} (file: ${file.name})"
    }
}

greetingFile = layout.buildDirectory.file('hello.txt')
$ gradle -q sayGreeting
Hello! (file: hello.txt)

在此範例中,我們將 greet 任務 destination 屬性配置為閉包/提供器,它會使用 Project.file(java.lang.Object) 方法進行評估,以在最後一刻將閉包/提供器的傳回值轉換為 File 物件。請注意,我們在任務配置之後指定了 greetingFile 屬性值。這種延遲評估是接受任何值來設定檔案屬性,然後在讀取屬性時解析該值的關鍵優勢。

您可以在 使用檔案 中了解更多關於延遲使用檔案的資訊。

使用擴充功能讓外掛可配置

大多數外掛都提供配置選項,供建置腳本和其他外掛自訂外掛的運作方式。外掛透過使用擴充功能物件來做到這一點。

專案具有關聯的 ExtensionContainer 物件,其中包含已套用至專案的外掛的所有設定和屬性。您可以透過將擴充功能物件新增至此容器,為您的外掛提供配置。

擴充功能物件只是一個具有 Java Bean 屬性的物件,代表配置。

讓我們將 greeting 擴充功能物件新增至專案,讓您可以配置問候語

build.gradle.kts
interface GreetingPluginExtension {
    val message: Property<String>
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Add the 'greeting' extension object
        val extension = project.extensions.create<GreetingPluginExtension>("greeting")
        // Add a task that uses configuration from the extension object
        project.task("hello") {
            doLast {
                println(extension.message.get())
            }
        }
    }
}

apply<GreetingPlugin>()

// Configure the extension
the<GreetingPluginExtension>().message = "Hi from Gradle"
build.gradle
interface GreetingPluginExtension {
    Property<String> getMessage()
}

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        // Add the 'greeting' extension object
        def extension = project.extensions.create('greeting', GreetingPluginExtension)
        // Add a task that uses configuration from the extension object
        project.task('hello') {
            doLast {
                println extension.message.get()
            }
        }
    }
}

apply plugin: GreetingPlugin

// Configure the extension
greeting.message = 'Hi from Gradle'
$ gradle -q hello
Hi from Gradle

在此範例中,GreetingPluginExtension 是一個具有名為 message 的屬性的物件。擴充功能物件會以名稱 greeting 新增至專案。此物件會以與擴充功能物件相同的名稱作為專案屬性提供。the<GreetingPluginExtension>() 等同於 project.extensions.getByType(GreetingPluginExtension::class.java)

通常,您需要在單個外掛上指定多個相關屬性。Gradle 為每個擴充功能物件新增一個配置區塊,因此您可以將設定分組

build.gradle.kts
interface GreetingPluginExtension {
    val message: Property<String>
    val greeter: Property<String>
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val extension = project.extensions.create<GreetingPluginExtension>("greeting")
        project.task("hello") {
            doLast {
                println("${extension.message.get()} from ${extension.greeter.get()}")
            }
        }
    }
}

apply<GreetingPlugin>()

// Configure the extension using a DSL block
configure<GreetingPluginExtension> {
    message = "Hi"
    greeter = "Gradle"
}
build.gradle
interface GreetingPluginExtension {
    Property<String> getMessage()
    Property<String> getGreeter()
}

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

apply plugin: GreetingPlugin

// Configure the extension using a DSL block
greeting {
    message = 'Hi'
    greeter = 'Gradle'
}
$ gradle -q hello
Hi from Gradle

在此範例中,可以在 configure<GreetingPluginExtension> 區塊中分組多個設定。configure 函數用於配置擴充功能物件。它提供了一種方便的方式來設定屬性或將配置套用至這些物件。在建置腳本的 configure 函數中使用的類型 (GreetingPluginExtension) 必須與擴充功能類型相符。然後,當區塊執行時,區塊的接收者就是擴充功能。

在此範例中,可以在 greeting 閉包中分組多個設定。建置腳本中閉包區塊的名稱 (greeting) 必須與擴充功能物件名稱相符。然後,當閉包執行時,擴充功能物件上的欄位將根據標準 Groovy 閉包委派功能對應到閉包內的變數。

宣告 DSL 配置容器

使用擴充功能物件擴充了 Gradle DSL,為外掛新增專案屬性和 DSL 區塊。由於擴充功能物件是常規物件,您可以透過將屬性和方法新增至擴充功能物件,在外掛區塊內提供您自己的巢狀 DSL。

讓我們考慮以下建置腳本以進行說明。

build.gradle.kts
plugins {
    id("org.myorg.server-env")
}

environments {
    create("dev") {
        url = "https://127.0.0.1:8080"
    }

    create("staging") {
        url = "http://staging.enterprise.com"
    }

    create("production") {
        url = "http://prod.enterprise.com"
    }
}
build.gradle
plugins {
    id 'org.myorg.server-env'
}

environments {
    dev {
        url = 'https://127.0.0.1:8080'
    }

    staging {
        url = 'http://staging.enterprise.com'
    }

    production {
        url = 'http://prod.enterprise.com'
    }
}

外掛公開的 DSL 公開了一個容器,用於定義一組環境。使用者配置的每個環境都有一個任意但宣告式的名稱,並以其自己的 DSL 配置區塊表示。上面的範例實例化了開發、預備和生產環境,包括其各自的 URL。

每個環境都必須在程式碼中具有資料表示形式,以擷取值。環境名稱是不可變的,可以作為建構子參數傳入。目前,資料物件儲存的唯一其他參數是 URL。

以下 ServerEnvironment 物件符合這些需求

ServerEnvironment.java
abstract public class ServerEnvironment {
    private final String name;

    @javax.inject.Inject
    public ServerEnvironment(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    abstract public Property<String> getUrl();
}

Gradle 公開了 factory 方法 ObjectFactory.domainObjectContainer(Class, NamedDomainObjectFactory) 來建立資料物件容器。方法採用的參數是代表資料的類別。類型 NamedDomainObjectContainer 的已建立實例可以透過使用特定名稱將其新增至擴充功能容器來向最終使用者公開。

外掛通常會在外掛實作中對擷取的值進行後處理,例如,配置任務

ServerEnvironmentPlugin.java
public class ServerEnvironmentPlugin implements Plugin<Project> {
    @Override
    public void apply(final Project project) {
        ObjectFactory objects = project.getObjects();

        NamedDomainObjectContainer<ServerEnvironment> serverEnvironmentContainer =
            objects.domainObjectContainer(ServerEnvironment.class, name -> objects.newInstance(ServerEnvironment.class, name));
        project.getExtensions().add("environments", serverEnvironmentContainer);

        serverEnvironmentContainer.all(serverEnvironment -> {
            String env = serverEnvironment.getName();
            String capitalizedServerEnv = env.substring(0, 1).toUpperCase() + env.substring(1);
            String taskName = "deployTo" + capitalizedServerEnv;
            project.getTasks().register(taskName, Deploy.class, task -> task.getUrl().set(serverEnvironment.getUrl()));
        });
    }
}

在上面的範例中,會為每個使用者配置的環境動態建立部署任務。

您可以在 開發自訂 Gradle 類型 中找到更多關於實作專案擴充功能的資訊。

塑模 DSL 類型的 API

外掛公開的 DSL 應易於閱讀和理解。

例如,讓我們考慮由外掛提供的以下擴充功能。在其目前形式中,它提供了一個「扁平」的屬性列表,用於配置網站的建立

build-flat.gradle.kts
plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = "https://gradle.org"
    vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}
build-flat.gradle
plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = 'https://gradle.org'
    vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
}

隨著公開屬性數量的增加,您應該引入巢狀、更具表現力的結構。

以下程式碼片段新增了一個名為 siteInfo 的新配置區塊作為擴充功能的一部分。這更強烈地表明了這些屬性的含義

build.gradle.kts
plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    siteInfo {
        websiteUrl = "https://gradle.org"
        vcsUrl = "https://github.com/gradle/gradle-site-plugin"
    }
}
build.gradle
plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    siteInfo {
        websiteUrl = 'https://gradle.org'
        vcsUrl = 'https://github.com/gradle/gradle-site-plugin'
    }
}

實作此類擴充功能的後端物件很簡單。首先,為管理屬性 websiteUrlvcsUrl 引入一個新的資料物件

SiteInfo.java
abstract public class SiteInfo {

    abstract public Property<String> getWebsiteUrl();

    abstract public Property<String> getVcsUrl();
}

在擴充功能中,建立 siteInfo 類別的實例和一個方法,將擷取的值委派給資料實例。

若要配置基礎資料物件,請定義 Action 類型的參數。

以下範例示範了在擴充功能定義中使用 Action

SiteExtension.java
abstract public class SiteExtension {

    abstract public RegularFileProperty getOutputDir();

    @Nested
    abstract public SiteInfo getSiteInfo();

    public void siteInfo(Action<? super SiteInfo> action) {
        action.execute(getSiteInfo());
    }
}

將擴充功能屬性對應到任務屬性

外掛通常使用擴充功能從建置腳本擷取使用者輸入,並將其對應到自訂任務的輸入/輸出屬性。建置腳本作者與擴充功能的 DSL 互動,而外掛實作處理基礎邏輯

app/build.gradle.kts
// Extension class to capture user input
class MyExtension {
    @Input
    var inputParameter: String? = null
}

// Custom task that uses the input from the extension
class MyCustomTask : org.gradle.api.DefaultTask() {
    @Input
    var inputParameter: String? = null

    @TaskAction
    fun executeTask() {
        println("Input parameter: $inputParameter")
    }
}

// Plugin class that configures the extension and task
class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Create and configure the extension
        val extension = project.extensions.create("myExtension", MyExtension::class.java)
        // Create and configure the custom task
        project.tasks.register("myTask", MyCustomTask::class.java) {
            group = "custom"
            inputParameter = extension.inputParameter
        }
    }
}
app/build.gradle
// Extension class to capture user input
class MyExtension {
    @Input
    String inputParameter = null
}

// Custom task that uses the input from the extension
class MyCustomTask extends DefaultTask {
    @Input
    String inputParameter = null

    @TaskAction
    def executeTask() {
        println("Input parameter: $inputParameter")
    }
}

// Plugin class that configures the extension and task
class MyPlugin implements Plugin<Project> {
    void apply(Project project) {
        // Create and configure the extension
        def extension = project.extensions.create("myExtension", MyExtension)
        // Create and configure the custom task
        project.tasks.register("myTask", MyCustomTask) {
            group = "custom"
            inputParameter = extension.inputParameter
        }
    }
}

在此範例中,MyExtension 類別定義了一個可以在建置腳本中設定的 inputParameter 屬性。MyPlugin 類別配置此擴充功能,並使用其 inputParameter 值來配置 MyCustomTask 任務。然後,MyCustomTask 任務在其邏輯中使用此輸入參數。

您可以在 延遲配置 中了解更多關於您可以在任務實作和擴充功能中使用的類型。

使用慣例新增預設配置

外掛應在特定上下文中提供合理的預設值和標準,以減少使用者需要做出的決策數量。使用 project 物件,您可以定義預設值。這些稱為慣例

慣例是使用預設值初始化的屬性,可以由使用者在其建置腳本中覆寫。例如

build.gradle.kts
interface GreetingPluginExtension {
    val message: Property<String>
}

class GreetingPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        // Add the 'greeting' extension object
        val extension = project.extensions.create<GreetingPluginExtension>("greeting")
        extension.message.convention("Hello from GreetingPlugin")
        // Add a task that uses configuration from the extension object
        project.task("hello") {
            doLast {
                println(extension.message.get())
            }
        }
    }
}

apply<GreetingPlugin>()
build.gradle
interface GreetingPluginExtension {
    Property<String> getMessage()
}

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        // Add the 'greeting' extension object
        def extension = project.extensions.create('greeting', GreetingPluginExtension)
        extension.message.convention('Hello from GreetingPlugin')
        // Add a task that uses configuration from the extension object
        project.task('hello') {
            doLast {
                println extension.message.get()
            }
        }
    }
}

apply plugin: GreetingPlugin
$ gradle -q hello
Hello from GreetingPlugin

在此範例中,GreetingPluginExtension 是一個代表慣例的類別。message 屬性是慣例屬性,預設值為「Hello from GreetingPlugin」。

使用者可以在其建置腳本中覆寫此值

build.gradle.kts
GreetingPluginExtension {
    message = "Custom message"
}
build.gradle
GreetingPluginExtension {
    message = 'Custom message'
}
$ gradle -q hello
Custom message

將功能與慣例分離

在外掛中將功能與慣例分離,讓使用者可以選擇要套用哪些任務和慣例。

例如,Java Base 外掛提供不帶主觀意見(即,通用)的功能,如 SourceSets,而 Java 外掛新增了 Java 開發人員熟悉的任務和慣例,如 classesjarjavadoc

在設計您自己的外掛時,請考慮開發兩個外掛 — 一個用於功能,另一個用於慣例 — 以向使用者提供彈性。

在下面的範例中,MyPlugin 包含慣例,而 MyBasePlugin 定義功能。然後,MyPlugin 套用 MyBasePlugin,這稱為外掛組合。若要從另一個外掛套用外掛

MyBasePlugin.java
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyBasePlugin implements Plugin<Project> {
    public void apply(Project project) {
        // define capabilities
    }
}
MyPlugin.java
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPluginManager().apply(MyBasePlugin.class);

        // define conventions
    }
}

對外掛做出反應

Gradle 外掛實作中的常見模式是配置建置中現有外掛和任務的執行階段行為。

例如,外掛可以假設它套用至基於 Java 的專案,並自動重新配置標準來源目錄

InhouseStrongOpinionConventionJavaPlugin.java
public class InhouseStrongOpinionConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        // Careful! Eagerly appyling plugins has downsides, and is not always recommended.
        project.getPluginManager().apply(JavaPlugin.class);
        SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
        SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
        main.getJava().setSrcDirs(Arrays.asList("src"));
    }
}

此方法的缺點是它會自動強制專案套用 Java 外掛,對其施加強烈的主觀意見(即,降低彈性和通用性)。實際上,套用此外掛的專案甚至可能不處理 Java 程式碼。

外掛可以對使用專案套用 Java 外掛的事實做出反應,而不是自動套用 Java 外掛。只有在這種情況下,才會套用特定配置

InhouseConventionJavaPlugin.java
public class InhouseConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPluginManager().withPlugin("java", javaPlugin -> {
            SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
            SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
            main.getJava().setSrcDirs(Arrays.asList("src"));
        });
    }
}

如果沒有充分的理由假設使用專案具有預期的設定,則對外掛做出反應優於套用外掛。

相同的概念適用於任務類型

InhouseConventionWarPlugin.java
public class InhouseConventionWarPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getTasks().withType(War.class).configureEach(war ->
            war.setWebXml(project.file("src/someWeb.xml")));
    }
}

對建置功能做出反應

外掛可以存取建置中建置功能的狀態。建置功能 API 允許檢查使用者是否請求了特定的 Gradle 功能,以及該功能在目前建置中是否處於活動狀態。建置功能的一個範例是配置快取

有兩個主要使用案例

  • 在報告或統計資料中使用建置功能的狀態。

  • 透過停用不相容的外掛功能來逐步採用實驗性的 Gradle 功能。

以下是一個同時使用這兩種案例的外掛範例。

對建置功能做出反應
public abstract class MyPlugin implements Plugin<Project> {

    @Inject
    protected abstract BuildFeatures getBuildFeatures(); (1)

    @Override
    public void apply(Project p) {
        BuildFeatures buildFeatures = getBuildFeatures();

        Boolean configCacheRequested = buildFeatures.getConfigurationCache().getRequested() (2)
            .getOrNull(); // could be null if user did not opt in nor opt out
        String configCacheUsage = describeFeatureUsage(configCacheRequested);
        MyReport myReport = new MyReport();
        myReport.setConfigurationCacheUsage(configCacheUsage);

        boolean isolatedProjectsActive = buildFeatures.getIsolatedProjects().getActive() (3)
            .get(); // the active state is always defined
        if (!isolatedProjectsActive) {
            myOptionalPluginLogicIncompatibleWithIsolatedProjects();
        }
    }

    private String describeFeatureUsage(Boolean requested) {
        return requested == null ? "no preference" : requested ? "opt-in" : "opt-out";
    }

    private void myOptionalPluginLogicIncompatibleWithIsolatedProjects() {
    }
}
1 BuildFeatures 服務可以注入到外掛、任務和其他受管理類型中。
2 存取功能的 requested 狀態以進行報告。
3 使用功能的 active 狀態來停用不相容的功能。

建置功能屬性

BuildFeature 狀態屬性以 Provider<Boolean> 類型表示。

BuildFeature.getRequested() 建置功能的狀態決定使用者是否請求啟用或停用該功能。

requested 提供器值為

  • true — 使用者選擇加入使用該功能

  • false — 使用者選擇退出使用該功能

  • undefined — 使用者既未選擇加入也未選擇退出使用該功能

BuildFeature.getActive() 建置功能的狀態始終已定義。它代表功能在建置中的有效狀態。

active 提供器值為

  • true — 功能可能會以特定於功能的方式影響建置行為

  • false — 功能不會影響建置行為

請注意,active 狀態不依賴於 requested 狀態。即使使用者請求了某項功能,由於建置中使用的其他建置選項,它仍然可能未處於活動狀態。即使使用者未指定偏好,Gradle 也可以預設啟用某項功能。

使用自訂 dependencies 區塊

外掛可以在自訂區塊中提供相依性宣告,讓使用者以類型安全且內容感知的方式宣告相依性。

例如,自訂 dependencies 區塊讓外掛可以選擇有意義的名稱,以便一致地使用,而無需使用者了解並使用基礎 Configuration 名稱來新增相依性。

新增自訂 dependencies 區塊

若要新增自訂 dependencies 區塊,您需要建立一個新的類型,該類型將代表使用者可用的相依性範圍集。需要可以從外掛的一部分(從網域物件或擴充功能)存取新的類型。最後,需要將相依性範圍接回將在相依性解析期間使用的基礎 Configuration 物件。

請參閱 JvmComponentDependenciesJvmTestSuite,以取得如何在 Gradle 核心外掛中使用此功能的範例。

1. 建立擴充 Dependencies 的介面

您也可以擴充 GradleDependencies 以存取 Gradle 提供的相依性,如 gradleApi()
ExampleDependencies.java
/**
 * Custom dependencies block for the example plugin.
 */
public interface ExampleDependencies extends Dependencies {

2. 為相依性範圍新增存取器

對於您的外掛想要支援的每個相依性範圍,新增一個傳回 DependencyCollector 的 getter 方法。

ExampleDependencies.java
    /**
     * Dependency scope called "implementation"
     */
    DependencyCollector getImplementation();

3. 為自訂 dependencies 區塊新增存取器

若要讓自訂 dependencies 區塊可配置,外掛需要新增一個 getDependencies 方法,該方法從上方傳回新的類型,以及一個名為 dependencies 的可配置區塊方法。

依照慣例,自訂 dependencies 區塊的存取器應稱為 getDependencies()/dependencies(Action)。此方法可以命名為其他名稱,但使用者需要知道不同的區塊可以像 dependencies 區塊一樣運作。

ExampleExtension.java
    /**
     * Custom dependencies for this extension.
     */
    @Nested
    ExampleDependencies getDependencies();

    /**
     * Configurable block
     */
    default void dependencies(Action<? super ExampleDependencies> action) {
        action.execute(getDependencies());
    }

4. 將相依性範圍接線到 Configuration

最後,外掛需要將自訂 dependencies 區塊接線到一些基礎 Configuration 物件。如果未完成此操作,則自訂區塊中宣告的相依性將無法用於相依性解析。

ExamplePlugin.java
        project.getConfigurations().dependencyScope("exampleImplementation", conf -> {
            conf.fromDependencyCollector(example.getDependencies().getImplementation());
        });
在此範例中,使用者將用來新增相依性的名稱是「implementation」,但基礎 Configuration 的名稱為 exampleImplementation
build.gradle.kts
example {
    dependencies {
        implementation("junit:junit:4.13")
    }
}
build.gradle
example {
    dependencies {
        implementation("junit:junit:4.13")
    }
}

自訂 dependencies 區塊與頂層 dependencies 區塊之間的差異

每個相依性範圍都會傳回一個 DependencyCollector,它提供強型別的方法來新增和配置相依性。

還有一個 DependencyFactory,其中包含 factory 方法,可從不同的表示法建立新的相依性。可以使用這些 factory 方法延遲建立相依性,如下所示。

自訂 dependencies 區塊與頂層 dependencies 區塊的不同之處在於以下幾點

  • 必須使用 StringDependency 的實例、FileCollectionDependencyProviderMinimalExternalModuleDependencyProviderConvertible 來宣告相依性。

  • 在 Gradle 建置腳本之外,您必須明確呼叫 DependencyCollector 的 getter 和 add

    • dependencies.add("implementation", x) 變成 getImplementation().add(x)

  • 您無法使用 Kotlin 和 Java 中的 Map 表示法宣告相依性。請改為在 Kotlin 和 Java 中使用多引數方法。

    • Kotlin:compileOnly(mapOf("group" to "foo", "name" to "bar")) 變成 compileOnly(module(group = "foo", name = "bar"))

    • Java:compileOnly(Map.of("group", "foo", "name", "bar")) 變成 getCompileOnly().add(module("foo", "bar", null))

  • 您無法新增 Project 實例的相依性。您必須先將其轉換為 ProjectDependency

  • 您無法直接新增版本目錄捆綁包。請改為在每個配置上使用 bundle 方法。

    • Kotlin 和 Groovy:implementation(libs.bundles.testing) 變成 implementation.bundle(libs.bundles.testing)

  • 您不能直接對非 Dependency 類型使用 providers。而是要使用 DependencyFactory 將它們映射到 Dependency

    • Kotlin 和 Groovy:implementation(myStringProvider) 變成 implementation(myStringProvider.map { dependencyFactory.create(it) })

    • Java:implementation(myStringProvider) 變成 getImplementation().add(myStringProvider.map(getDependencyFactory()::create)

  • 與頂層的 dependencies 區塊不同,constraints 並不在單獨的區塊中。

    • 而是透過使用 constraint(…​) 修飾 dependency 來新增 constraints,例如 implementation(constraint("org:foo:1.0"))

請注意,dependencies 區塊可能無法提供與 頂層 dependencies 區塊 相同的方法。

外掛程式應優先透過它們自己的 dependencies 區塊新增 dependencies。

提供預設 dependencies

外掛程式的實作有時需要使用外部 dependency。

您可能希望使用 Gradle 的 dependency 管理機制自動下載 artifact,然後在 plugin 中宣告的 task 類型的 action 中使用它。理想情況下,外掛程式實作不需要使用者提供該 dependency 的座標 - 它可以簡單地預先定義一個合理的預設版本。

讓我們來看一個外掛程式範例,該外掛程式下載包含用於進一步處理之資料的檔案。外掛程式實作宣告了一個自訂配置,允許使用 dependency 座標指派這些外部 dependencies

DataProcessingPlugin.java
public class DataProcessingPlugin implements Plugin<Project> {
    public void apply(Project project) {
        Configuration dataFiles = project.getConfigurations().create("dataFiles", c -> {
            c.setVisible(false);
            c.setCanBeConsumed(false);
            c.setCanBeResolved(true);
            c.setDescription("The data artifacts to be processed for this plugin.");
            c.defaultDependencies(d -> d.add(project.getDependencies().create("org.myorg:data:1.4.6")));
        });

        project.getTasks().withType(DataProcessing.class).configureEach(
            dataProcessing -> dataProcessing.getDataFiles().from(dataFiles));
    }
}
DataProcessing.java
abstract public class DataProcessing extends DefaultTask {

    @InputFiles
    abstract public ConfigurableFileCollection getDataFiles();

    @TaskAction
    public void process() {
        System.out.println(getDataFiles().getFiles());
    }
}

對於最終使用者來說,這種方法很方便,因為無需主動宣告 dependency。外掛程式已經提供了有關此實作的所有詳細資訊。

但是,如果使用者想要重新定義預設 dependency 呢?

沒問題。外掛程式也公開了可以用於指派不同 dependency 的自訂配置。實際上,預設 dependency 會被覆寫。

build.gradle.kts
plugins {
    id("org.myorg.data-processing")
}

dependencies {
    dataFiles("org.myorg:more-data:2.6")
}
build.gradle
plugins {
    id 'org.myorg.data-processing'
}

dependencies {
    dataFiles 'org.myorg:more-data:2.6'
}

您會發現這種模式非常適用於在執行 task 的 action 時需要外部 dependency 的 tasks。您可以更進一步,透過公開 extension 屬性(例如JaCoCo 外掛程式中的 toolVersion)來抽象化用於外部 dependency 的版本。

盡量減少外部函式庫的使用

在您的 Gradle 專案中使用外部函式庫可以帶來極大的便利,但請注意,它們可能會引入複雜的 dependency graphs。Gradle 的 buildEnvironment task 可以幫助您視覺化這些 dependencies,包括您的外掛程式的 dependencies。請記住,外掛程式共用相同的 classloader,因此相同函式庫的不同版本可能會產生衝突。

為了示範,讓我們假設以下 build script

build.gradle.kts
plugins {
    id("org.asciidoctor.jvm.convert") version "4.0.2"
}
build.gradle
plugins {
    id 'org.asciidoctor.jvm.convert' version '4.0.2'
}

task 的輸出清楚地指示了 classpath 配置的 classpath

$ gradle buildEnvironment

> Task :buildEnvironment

------------------------------------------------------------
Root project 'external-libraries'
------------------------------------------------------------

classpath
\--- org.asciidoctor.jvm.convert:org.asciidoctor.jvm.convert.gradle.plugin:4.0.2
     \--- org.asciidoctor:asciidoctor-gradle-jvm:4.0.2
          +--- org.ysb33r.gradle:grolifant-rawhide:3.0.0
          |    \--- org.tukaani:xz:1.6
          +--- org.ysb33r.gradle:grolifant-herd:3.0.0
          |    +--- org.tukaani:xz:1.6
          |    +--- org.ysb33r.gradle:grolifant40:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.apache.commons:commons-collections4:4.4
          |    |    +--- org.ysb33r.gradle:grolifant-core:3.0.0
          |    |    |    +--- org.tukaani:xz:1.6
          |    |    |    +--- org.apache.commons:commons-collections4:4.4
          |    |    |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant50:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant40-legacy-api:3.0.0
          |    |         +--- org.tukaani:xz:1.6
          |    |         +--- org.apache.commons:commons-collections4:4.4
          |    |         +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    |         \--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant60:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant50:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant70:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant50:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant60:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant80:3.0.0
          |    |    +--- org.tukaani:xz:1.6
          |    |    +--- org.ysb33r.gradle:grolifant40:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant50:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant60:3.0.0 (*)
          |    |    +--- org.ysb33r.gradle:grolifant70:3.0.0 (*)
          |    |    \--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    +--- org.ysb33r.gradle:grolifant-core:3.0.0 (*)
          |    \--- org.ysb33r.gradle:grolifant-rawhide:3.0.0 (*)
          +--- org.asciidoctor:asciidoctor-gradle-base:4.0.2
          |    \--- org.ysb33r.gradle:grolifant-herd:3.0.0 (*)
          \--- org.asciidoctor:asciidoctorj-api:2.5.7

(*) - Indicates repeated occurrences of a transitive dependency subtree. Gradle expands transitive dependency subtrees only once per project; repeat occurrences only display the root of the subtree, followed by this annotation.

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 0s
1 actionable task: 1 executed

Gradle 外掛程式不會在其自己的隔離 classloader 中執行,因此您必須考慮是否真的需要函式庫,或者更簡單的解決方案是否足夠。

對於作為 task 執行一部分執行的邏輯,請使用Worker API,它允許您隔離函式庫。

提供外掛程式的多個變體

外掛程式的變體指的是針對特定需求或使用案例量身定制的外掛程式的不同風格或配置。這些變體可以包括基礎外掛程式的不同實作、extensions 或配置。

配置額外外掛程式變體最方便的方法是使用feature variants,這個概念在所有套用 Java 外掛程式之一的 Gradle 專案中都可用。

dependencies {
    implementation 'com.google.guava:guava:30.1-jre'        // Regular dependency
    featureVariant 'com.google.guava:guava-gwt:30.1-jre'    // Feature variant dependency
}

在以下範例中,每個外掛程式變體都是隔離開發的。一個單獨的 source set 被編譯並打包在每個變體的單獨 jar 中。

以下範例示範如何新增一個與 Gradle 7.0+ 相容的變體,而 "main" 變體與舊版本相容

build.gradle.kts
val gradle7 = sourceSets.create("gradle7")

java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) (1)
    }
}

configurations.configureEach {
    if (isCanBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
                objects.named("7.0"))
        }
    }
}

tasks.named<Copy>(gradle7.processResourcesTaskName) { (3)
    val copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into("META-INF/gradle-plugins")
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    "gradle7CompileOnly"(gradleApi()) (4)
}
build.gradle
def gradle7 = sourceSets.create('gradle7')

java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) (1)
    }
}

configurations.configureEach {
    if (canBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
                      objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}

tasks.named(gradle7.processResourcesTaskName) { (3)
    def copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into('META-INF/gradle-plugins')
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    gradle7CompileOnly(gradleApi()) (4)
}
只有 Gradle 7 或更高版本可以明確地作為變體的目標,因為對此的支援僅在 Gradle 7 中新增。

首先,我們為我們的 Gradle 7 外掛程式變體宣告一個單獨的 source set 和一個 feature variant。然後,我們進行一些特定的 wiring,將 feature 變成一個適當的 Gradle 外掛程式變體

1 對應於 components GAV 的隱含 capability 指派給變體。
2 Gradle API 版本屬性指派給我們 Gradle7 變體的所有consumable configurations。Gradle 使用此資訊來決定在外掛程式解析期間選擇哪個變體。
3 配置 processGradle7Resources task 以確保外掛程式描述符檔案被新增到 Gradle7 變體 Jar 中。
4 為我們的新變體新增對 gradleApi() 的 dependency,以便在編譯時可以看到 API。

請注意,目前沒有方便的方法可以像您用來建置外掛程式的 API 那樣存取其他 Gradle 版本的 API。理想情況下,每個變體都應該能夠宣告對其支援的最小 Gradle 版本 API 的 dependency。這將在未來得到改進。

上述程式碼片段假設您的外掛程式的所有變體都將外掛程式類別放在相同的位置。也就是說,如果您的外掛程式類別是 org.example.GreetingPlugin,您需要在 src/gradle7/java/org/example 中建立該類別的第二個變體。

使用多變體外掛程式的版本特定變體

如果 dependency 是一個多變體外掛程式,當 Gradle 解析以下任何項時,它會自動選擇最符合目前 Gradle 版本的變體:

最佳匹配變體是目標 Gradle API 版本最高且不超過目前 build 的 Gradle 版本的變體。

在所有其他情況下,如果存在不指定支援的 Gradle API 版本的外掛程式變體,則優先選擇該變體。

在使用外掛程式作為 dependencies 的專案中,可以請求支援不同 Gradle 版本的外掛程式 dependencies 的變體。這允許依賴其他外掛程式的多變體外掛程式存取其 API,這些 API 專門在其版本特定的變體中提供。

此程式碼片段使上面定義的 gradle7 外掛程式變體使用其對其他多變體外掛程式的 dependencies 的匹配變體

build.gradle.kts
configurations.configureEach {
    if (isCanBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named("7.0"))
        }
    }
}
build.gradle
configurations.configureEach {
    if (canBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}