二進位外掛程式是指編譯並以 JAR 檔案形式發佈的外掛程式。這些外掛程式通常以 Java 或 Kotlin 編寫,並提供自訂功能或工作給 Gradle 建置。

使用外掛程式開發外掛程式

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

此外掛程式會自動套用Java 外掛程式,將gradleApi()相依性新增至api設定,產生結果 JAR 檔案中所需的 plugin 描述符,並設定外掛程式標記人工製品,以便在發佈時使用。

若要套用並設定外掛程式,請將下列程式碼新增至您的建置檔案

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 的 managed propertiesproject.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 公開工廠方法 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'
}

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

下列程式碼片段新增一個名為 customData 的新組態區塊,作為擴充的一部分。這提供了這些屬性的意義的更強指示

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

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

    customData {
        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")

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

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

CustomData.java
abstract public class CustomData {

    abstract public Property<String> getWebsiteUrl();

    abstract public Property<String> getVcsUrl();
}

在擴充功能中,建立 CustomData 類別的執行個體,以及將擷取的值委派給資料執行個體的方法。

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

下列範例示範如何在擴充功能定義中使用 Action

SiteExtension.java
abstract public class SiteExtension {

    abstract public RegularFileProperty getOutputDir();

    @Nested
    abstract public CustomData getCustomData();

    public void customData(Action<? super CustomData> action) {
        action.execute(getCustomData());
    }
}

將擴充功能屬性對應至工作屬性

外掛程式通常會使用擴充功能來擷取建置指令碼中的使用者輸入,並將其對應至自訂工作的輸入/輸出屬性。建置指令碼作者會與擴充功能的 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 是代表慣例的類別。訊息屬性是慣例屬性,預設值為 '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.getPlugins().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.getPlugins().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.getPlugins().withType(JavaPlugin.class, 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 也可以預設啟用某個功能。

提供預設相依性

插件的實作有時需要使用外部相依性。

您可能想要使用 Gradle 的相依性管理機制自動下載人工製品,並稍後在插件中宣告的工作類型動作中使用它。理想情況下,插件實作不需要向使用者詢問該相依性的座標 - 它可以簡單地預先定義一個合理的預設版本。

我們來看一個下載包含資料檔案以供進一步處理的插件範例。插件實作宣告一個自訂組態,允許使用相依性座標指派那些外部相依性

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());
    }
}

此方法對最終使用者而言很方便,因為不需要主動宣告相依性。此外掛程式已提供此實作的所有詳細資料。

但如果使用者想要重新定義預設相依性呢?

沒問題。此外掛程式也會公開自訂組態,可用於指定不同的相依性。實際上,預設相依性會被覆寫

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'
}

您會發現此模式非常適合在執行工作時需要外部相依性的工作。您可以進一步抽象外部相依性所使用的版本,方法是公開延伸屬性(例如 JaCoCo 外掛程式 中的 toolVersion)。

將外部函式庫的使用降至最低

在 Gradle 專案中使用外部函式庫可以帶來極大的便利性,但請注意它們可能會引入複雜的相依性圖。Gradle 的 buildEnvironment 工作可以協助您視覺化這些相依性,包括外掛程式的相依性。請記住,外掛程式共用同一個類別載入器,因此不同版本的同一個函式庫可能會產生衝突。

為了示範,我們假設有下列建置指令碼

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'
}

此工作的輸出清楚指出 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 外掛程式並非在自己的獨立類別載入器中執行,因此您必須考慮是否真的需要函式庫,或者是否有更簡單的解決方案就已足夠。

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

提供外掛程式的多個變體

設定額外外掛程式變體最方便的方法是使用 功能變體,這是適用於套用其中一個 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
}

在以下範例中,每個外掛變異都是獨立開發的。針對每個變異,會編譯一個獨立的來源設定集,並封裝在一個獨立的 jar 中。

以下範例說明如何新增與 Gradle 7.0+ 相容的變異,而「主要」變異則與舊版本相容

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 中。

首先,我們為 Gradle7 外掛變異宣告一個獨立的來源設定集和一個功能變異。然後,我們執行一些特定配線,以將功能轉換為適當的 Gradle 外掛變異

1 對應於元件 GAV 的隱含功能指派給變異。
2 Gradle API 版本屬性指派給 Gradle7 變異的所有可消耗組態。Gradle 會使用此資訊來決定在外掛解析期間要選取哪個變異。
3 設定 processGradle7Resources 工作,以確保將外掛描述符檔案新增至 Gradle7 變異 Jar。
4 對新變異新增對 gradleApi() 的依賴關係,以便在編譯期間可以看到 API。

請注意,目前沒有方便的方法可以存取與您用來建置外掛的 Gradle 版本相同的外掛 API。理想情況下,每個變異都應該能夠宣告對其支援的最低 Gradle 版本的 API 的依賴關係。這將在未來進行改善。

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

使用多重變異外掛的版本特定變異

在依賴於多重變異外掛的情況下,當 Gradle 解析任何下列項目時,它會自動選擇最符合目前 Gradle 版本的變異

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

在所有其他情況下,如果存在未指定支援的 Gradle API 版本的外掛變體,則優先使用該變體。

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

此程式碼片段讓 上方定義的外掛變體 gradle7 使用其對其他多變體外掛的相依性的匹配變體

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'))
        }
    }
}

回報問題

外掛可透過 Gradle 的問題回報 API 回報問題。這些 API 會回報建置期間發生問題的豐富結構化資訊。不同使用者介面(例如 Gradle 的主控台輸出、建置掃描或 IDE)可以使用這些資訊,以最合適的方式向使用者傳達問題。

下列範例顯示外掛回報的問題

ProblemReportingPlugin.java
public class ProblemReportingPlugin implements Plugin<Project> {

    private final ProblemReporter problemReporter;

    @Inject
    public ProblemReportingPlugin(Problems problems) { (1)
        this.problemReporter = problems.forNamespace("org.myorg"); (2)
    }

    public void apply(Project project) {
        this.problemReporter.reporting(builder -> builder (3)
            .label("Plugin 'x' is deprecated")
            .details("The plugin 'x' is deprecated since version 2.5")
            .solution("Please use plugin 'y'")
            .severity(Severity.WARNING)
        );
    }
}
1 Problem 服務會注入到外掛中。
2 會為外掛建立問題回報者。雖然命名空間由外掛作者決定,但建議使用外掛 ID。
3 會回報問題。這個問題是可以復原的,因此建置會繼續進行。

如需完整範例,請參閱我們的 端對端範例

建置問題

回報問題時,可以提供各種資訊。ProblemSpec 說明可以提供的全部資訊。

回報問題

關於報告問題,我們支援三種不同的模式

  • 報告問題用於報告可復原的問題,且建置應繼續進行。

  • 拋出問題用於報告不可復原的問題,且建置應失敗。

  • 重新拋出問題用於包裝已拋出的例外狀況。否則,行為與「拋出」相同。

有關更多詳細資料,請參閱 ProblemReporter 文件。

問題彙總

報告問題時,Gradle 會根據問題的類別標籤,透過 Tooling API 傳送類似問題來彙總這些問題。

  • 報告問題時,第一次出現的問題會報告為 ProblemDescriptor,其中包含問題的完整資訊。

  • 同一個問題的任何後續出現,都會報告為 ProblemAggregationDescriptor。此描述符會在建置的結尾到達,並包含問題出現的次數。

  • 如果任何區塊(即類別和標籤配對)的收集出現次數大於 10.000,則會立即傳送,而不是在建置結尾傳送。