Java 程式庫外掛程式透過提供關於 Java 程式庫的特定知識,擴展了 Java 外掛程式 (java) 的功能。特別是,Java 程式庫會向消費者公開 API(即,使用 Java 或 Java 程式庫外掛程式的其他專案)。使用此外掛程式時,Java 外掛程式公開的所有原始碼集、任務和組態都會隱含地可用。

用法

若要使用 Java 程式庫外掛程式,請在您的建置腳本中包含以下內容

build.gradle.kts
plugins {
    `java-library`
}
build.gradle
plugins {
    id 'java-library'
}

API 與實作分離

標準 Java 外掛程式與 Java 程式庫外掛程式之間的關鍵差異在於後者引入了向消費者公開的 API 概念。程式庫是一種 Java 元件,旨在供其他元件使用。這在多專案建置中是一個非常常見的用例,並且在您擁有外部相依性時也是如此。

此外掛程式公開了兩個 組態,可用於宣告相依性:apiimplementationapi 組態應用於宣告由程式庫 API 匯出的相依性,而 implementation 組態應用於宣告元件內部的相依性。

build.gradle.kts
dependencies {
    api("org.apache.httpcomponents:httpclient:4.5.7")
    implementation("org.apache.commons:commons-lang3:3.5")
}
build.gradle
dependencies {
    api 'org.apache.httpcomponents:httpclient:4.5.7'
    implementation 'org.apache.commons:commons-lang3:3.5'
}

出現在 api 組態中的相依性將以遞移方式公開給程式庫的消費者,因此將出現在消費者的編譯類別路徑中。另一方面,在 implementation 組態中找到的相依性將不會公開給消費者,因此不會洩漏到消費者的編譯類別路徑中。這帶來了幾個好處

  • 相依性不再洩漏到消費者的編譯類別路徑中,因此您永遠不會意外地依賴遞移性相依性

  • 由於類別路徑大小縮減,編譯速度更快

  • 當實作相依性變更時,重新編譯次數減少:消費者無需重新編譯

  • 更簡潔的發佈:當與新的 maven-publish 外掛程式結合使用時,Java 程式庫會產生 POM 檔案,該檔案明確區分了針對程式庫進行編譯所需的内容和在執行時期使用程式庫所需的内容(換句話說,不要混淆編譯程式庫本身所需的内容和針對程式庫進行編譯所需的内容)。

compileruntime 組態已在 Gradle 7.0 中移除。請參閱升級指南,了解如何遷移到 implementationapi 組態。

如果您的建置使用具有 POM 元資料的已發佈模組,則 Java 和 Java 程式庫外掛程式都會透過 POM 中使用的範圍來遵守 api 和實作分離。這表示編譯類別路徑僅包含 Maven compile 範圍的相依性,而執行時期類別路徑也會加入 Maven runtime 範圍的相依性。

這通常對使用 Maven 發佈的模組沒有影響,在這種情況下,定義專案的 POM 會直接發佈為元資料。在那裡,編譯範圍既包含編譯專案所需的相依性(即實作相依性),也包含針對已發佈程式庫進行編譯所需的相依性(即 API 相依性)。對於大多數已發佈的程式庫,這表示所有相依性都屬於編譯範圍。如果您在現有的程式庫中遇到此類問題,您可以考慮使用元件元資料規則來修正建置中不正確的元資料。但是,如上所述,如果程式庫是使用 Gradle 發佈的,則產生的 POM 檔案僅將 api 相依性放入編譯範圍,並將剩餘的 implementation 相依性放入執行時期範圍。

如果您的建置使用具有 Ivy 元資料的模組,如果所有模組都遵循特定結構,您也許能夠按照此處所述啟用 api 和實作分離。

預設情況下,在 Gradle 5.0+ 中,模組的編譯和執行時期範圍分離已啟用。在 Gradle 4.6+ 中,您需要透過在 settings.gradle 中新增 enableFeaturePreview('IMPROVED_POM_SUPPORT') 來啟用它。

識別 API 和實作相依性

本節將協助您使用簡單的經驗法則來識別程式碼中的 API 和實作相依性。其中第一個是

  • 在可能的情況下,優先選擇 implementation 組態而非 api

這可讓相依性遠離消費者的編譯類別路徑。此外,如果任何實作類型意外洩漏到公用 API 中,消費者將立即編譯失敗。

那麼,何時應該使用 api 組態?API 相依性是指包含至少一種在程式庫二進制介面中公開的類型,通常稱為其 ABI(應用程式二進制介面)的相依性。這包括但不限於

  • 在超類別或介面中使用的類型

  • 在公用方法參數中使用的類型,包括泛型參數類型(其中 public 是編譯器可見的內容。即,Java 世界中的 publicprotectedpackage private 成員)

  • 在公用欄位中使用的類型

  • 公用註解類型

相比之下,在以下列表中使用的任何類型都與 ABI 無關,因此應宣告為 implementation 相依性

  • 僅在方法主體中使用的類型

  • 僅在私有成員中使用的類型

  • 僅在內部類別中找到的類型(Gradle 的未來版本將允許您宣告哪些套件屬於公用 API)

以下類別使用了幾個第三方程式庫,其中一個在類別的公用 API 中公開,另一個僅在內部使用。import 語句無助於我們確定哪個是哪個,因此我們必須查看欄位、建構子和方法。

範例:區分 API 和實作

src/main/java/org/gradle/HttpClientWrapper.java
// The following types can appear anywhere in the code
// but say nothing about API or implementation usage
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class HttpClientWrapper {

    private final HttpClient client; // private member: implementation details

    // HttpClient is used as a parameter of a public method
    // so "leaks" into the public API of this component
    public HttpClientWrapper(HttpClient client) {
        this.client = client;
    }

    // public methods belongs to your API
    public byte[] doRawGet(String url) {
        HttpGet request = new HttpGet(url);
        try {
            HttpEntity entity = doGet(request);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            entity.writeTo(baos);
            return baos.toByteArray();
        } catch (Exception e) {
            ExceptionUtils.rethrow(e); // this dependency is internal only
        } finally {
            request.releaseConnection();
        }
        return null;
    }

    // HttpGet and HttpEntity are used in a private method, so they don't belong to the API
    private HttpEntity doGet(HttpGet get) throws Exception {
        HttpResponse response = client.execute(get);
        if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            System.err.println("Method failed: " + response.getStatusLine());
        }
        return response.getEntity();
    }
}

HttpClientWrapper公用建構子使用 HttpClient 作為參數,因此它會公開給消費者,並因此屬於 API。請注意,HttpGetHttpEntity 用於私有方法的簽名中,因此它們不計入使 HttpClient 成為 API 相依性的因素。

另一方面,來自 commons-lang 程式庫的 ExceptionUtils 類型僅在方法主體中使用(不在其簽名中),因此它是實作相依性。

因此,我們可以推斷 httpclient 是 API 相依性,而 commons-lang 是實作相依性。此結論轉換為建置腳本中的以下宣告

build.gradle.kts
dependencies {
    api("org.apache.httpcomponents:httpclient:4.5.7")
    implementation("org.apache.commons:commons-lang3:3.5")
}
build.gradle
dependencies {
    api 'org.apache.httpcomponents:httpclient:4.5.7'
    implementation 'org.apache.commons:commons-lang3:3.5'
}

Java 程式庫外掛程式組態

下圖說明了在使用 Java 程式庫外掛程式時如何設定組態。

java library ignore deprecated main
  • 綠色中的組態是用戶應使用來宣告相依性的組態

  • 粉紅色中的組態是在元件編譯或針對程式庫執行時使用的組態

  • 藍色中的組態是元件內部的組態,供其自身使用

下圖說明了測試組態設定

java library ignore deprecated test

下表說明了每個組態的角色

表 1. Java 程式庫外掛程式 - 用於宣告相依性的組態
組態名稱 角色 可消耗? 可解析? 描述

annotationProcessor

宣告註解處理器

此組態用於宣告註解處理器,以確保它們在編譯階段可用於程式碼產生。

api

宣告 API 相依性

您可以在此處宣告以遞移方式匯出給消費者的相依性,以用於編譯時期和執行時期。

implementation

宣告實作相依性

您可以在此處宣告純粹是內部的且不打算公開給消費者的相依性(它們在執行時期仍然會公開給消費者)。

compileOnly

宣告僅編譯相依性

您可以在此處宣告在編譯時期需要但在執行時期不需要的相依性。這通常包括在執行時期找到時會被遮蔽的相依性。

compileOnlyApi

宣告僅編譯 API 相依性

您可以在此處宣告您的模組和消費者在編譯時期需要但在執行時期不需要的相依性。這通常包括在執行時期找到時會被遮蔽的相依性。

runtimeOnly

宣告執行時期相依性

您可以在此處宣告僅在執行時期需要且在編譯時期不需要的相依性。

testImplementation

測試相依性

您可以在此處宣告用於編譯測試的相依性。

testCompileOnly

宣告僅測試編譯相依性

您可以在此處宣告僅在測試編譯時期需要但不應洩漏到執行時期的相依性。這通常包括在執行時期找到時會被遮蔽的相依性。

testRuntimeOnly

宣告測試執行時期相依性

您可以在此處宣告僅在測試執行時期需要且在測試編譯時期不需要的相依性。

表 2. Java 程式庫外掛程式 — 消費者使用的組態
組態名稱 角色 可消耗? 可解析? 描述

apiElements

用於針對此程式庫進行編譯

此組態旨在供消費者使用,以檢索針對此程式庫進行編譯所需的所有元素。

runtimeElements

用於執行此程式庫

此組態旨在供消費者使用,以檢索針對此程式庫執行所需的所有元素。

表 3. Java 程式庫外掛程式 - 程式庫本身使用的組態
組態名稱 角色 可消耗? 可解析? 描述

compileClasspath

用於編譯此程式庫

此組態包含此程式庫的編譯類別路徑,因此在調用 java 編譯器來編譯它時使用。

runtimeClasspath

用於執行此程式庫

此組態包含此程式庫的執行時期類別路徑

testCompileClasspath

用於編譯此程式庫的測試

此組態包含此程式庫的測試編譯類別路徑。

testRuntimeClasspath

用於執行此程式庫的測試

此組態包含此程式庫的測試執行時期類別路徑

為 Java 模組系統建置模組

自 Java 9 以來,Java 本身提供了一個 模組系統,該系統允許在編譯和執行時期進行嚴格的封裝。您可以透過在 main/java 原始碼資料夾中建立 module-info.java 檔案,將 Java 程式庫轉換為 Java 模組

src
└── main
    └── java
        └── module-info.java

在模組資訊檔案中,您宣告模組名稱、您要匯出的模組套件以及您需要的其他模組。

module-info.java 檔案
module org.gradle.sample {
    requires com.google.gson;          // real module
    requires org.apache.commons.lang3; // automatic module
    // commons-cli-1.4.jar is not a module and cannot be required
}

為了告知 Java 編譯器 Jar 是一個模組,而不是傳統的 Java 程式庫,Gradle 需要將其放置在所謂的模組路徑上。它是類別路徑的替代方案,而類別路徑是告知編譯器關於已編譯相依性的傳統方式。如果以下三件事為真,Gradle 會自動將您的相依性 Jar 放置在模組路徑上,而不是類別路徑上

  • java.modularity.inferModulePath 關閉

  • 我們實際上正在建置一個模組(而不是傳統的程式庫),我們透過新增 module-info.java 檔案來表達這一點。(另一個選項是新增 Automatic-Module-Name Jar 資訊清單屬性,如下方所述。)

  • 我們的模組所依賴的 Jar 本身就是一個模組,Gradle 會根據 Jar 中是否存在 module-info.class(模組描述符的已編譯版本)來決定。(或者,另一種方法是 Jar 資訊清單中是否存在 Automatic-Module-Name 屬性)

在下文中,將描述關於定義 Java 模組以及它如何與 Gradle 的相依性管理互動的更多詳細資訊。您也可以查看現成的範例,以直接試用 Java 模組支援。

宣告模組相依性

在您在建置檔案中宣告的相依性和您在 module-info.java 檔案中宣告的模組相依性之間存在直接關係。理想情況下,宣告應保持同步,如下表所示。

表 4. Java 模組指示詞與 Gradle 組態之間用於宣告相依性的對應
Java 模組指示詞 Gradle 組態 目的

requires

implementation

宣告實作相依性

requires transitive

api

宣告 API 相依性

requires static

compileOnly

宣告僅編譯相依性

requires static transitive

compileOnlyApi

宣告僅編譯 API 相依性

Gradle 目前不會自動檢查相依性宣告是否同步。這可能會在未來版本中新增。

如需宣告模組相依性的更多詳細資訊,請參閱關於 Java 模組系統的文件

宣告套件可見性和服務

Java 模組系統支援比 Gradle 本身目前更多的細微封裝概念。例如,您需要明確宣告哪些套件是您的 API 的一部分,以及哪些套件僅在您的模組內部可見。其中一些功能可能會在未來版本中新增到 Gradle 本身。目前,請參閱關於 Java 模組系統的文件,以了解如何在 Java 模組中使用這些功能。

宣告模組版本

Java 模組也有一個版本,該版本編碼為 module-info.class 檔案中模組身分的一部分。模組執行時可以檢查此版本。

build.gradle.kts
version = "1.2"

tasks.compileJava {
    // use the project's version or define one directly
    options.javaModuleVersion = provider { version as String }
}
build.gradle
version = '1.2'

tasks.named('compileJava') {
    // use the project's version or define one directly
    options.javaModuleVersion = provider { version }
}

使用非模組的程式庫

您可能想要在您的模組化 Java 專案中使用外部程式庫,例如來自 Maven Central 的 OSS 程式庫。有些程式庫在其較新版本中已經是具有模組描述符的完整模組。例如,com.google.code.gson:gson:2.8.9 具有模組名稱 com.google.gson

其他程式庫,例如 org.apache.commons:commons-lang3:3.10,可能不提供完整的模組描述符,但至少會在資訊清單檔案中包含 Automatic-Module-Name 項目,以定義模組的名稱(範例中為 org.apache.commons.lang3)。這些模組僅具有作為模組描述的名稱,稱為自動模組,它們匯出所有套件,並且可以讀取模組路徑上的所有模組。

第三種情況是完全不提供模組資訊的傳統程式庫,例如 commons-cli:commons-cli:1.4。Gradle 會將此類程式庫放置在類別路徑上,而不是模組路徑上。然後,Java 會將類別路徑視為一個模組(所謂的未命名模組)。

build.gradle.kts
dependencies {
    implementation("com.google.code.gson:gson:2.8.9")       // real module
    implementation("org.apache.commons:commons-lang3:3.10") // automatic module
    implementation("commons-cli:commons-cli:1.4")           // plain library
}
build.gradle
dependencies {
    implementation 'com.google.code.gson:gson:2.8.9'       // real module
    implementation 'org.apache.commons:commons-lang3:3.10' // automatic module
    implementation 'commons-cli:commons-cli:1.4'           // plain library
}
在 module-info.java 檔案中宣告的模組相依性
module org.gradle.sample.lib {
    requires com.google.gson;          // real module
    requires org.apache.commons.lang3; // automatic module
    // commons-cli-1.4.jar is not a module and cannot be required
}

雖然真正的模組無法直接依賴未命名模組(僅能透過新增命令列標誌),但自動模組也可以看到未命名模組。因此,如果您無法避免依賴沒有模組資訊的程式庫,您可以將該程式庫包裝在自動模組中,作為專案的一部分。下一節將說明如何執行此操作。

處理非模組的另一種方法是使用 工件轉換,自行使用模組描述符來豐富現有的 Jar。 此範例包含一個小的 buildSrc 外掛程式,用於註冊此類轉換,您可以根據需要使用和調整它。如果您想要建置完全模組化應用程式,並希望 Java 執行時期將所有內容視為真正的模組,這可能會很有趣。

停用 Java 模組支援

在極少數情況下,您可能想要停用內建的 Java 模組支援,並透過其他方式定義模組路徑。若要實現此目的,您可以停用自動將任何 Jar 放置在模組路徑上的功能。然後,即使您的原始碼集中有 module-info.java,Gradle 也會將具有模組資訊的 Jar 放置在類別路徑上。這對應於 Gradle 版本 <7.0 的行為。

為了使此功能生效,您需要在 Java 擴充功能(對於所有任務)或個別任務上設定 modularity.inferModulePath = false

build.gradle.kts
java {
    modularity.inferModulePath = false
}

tasks.compileJava {
    modularity.inferModulePath = false
}
build.gradle
java {
    modularity.inferModulePath = false
}

tasks.named('compileJava') {
    modularity.inferModulePath = false
}

建置自動模組

如果可以,您應始終為您的模組編寫完整的 module-info.java 描述符。儘管如此,在少數情況下,您可能會考慮(最初)僅為自動模組提供模組名稱

  • 您正在開發一個不是模組的程式庫,但您希望在下一個版本中使其可用作模組。新增 Automatic-Module-Name 是一個好的第一步(Maven Central 上最受歡迎的 OSS 程式庫現在都已完成此操作)。

  • 如上一節所述,自動模組可以用作您的真實模組與類別路徑上的傳統程式庫之間的適配器。

若要將一般的 Java 專案轉換為自動模組,只需新增具有模組名稱的資訊清單項目

build.gradle.kts
tasks.jar {
    manifest {
        attributes("Automatic-Module-Name" to "org.gradle.sample")
    }
}
build.gradle
tasks.named('jar') {
    manifest {
        attributes('Automatic-Module-Name': 'org.gradle.sample')
    }
}
=== 您可以將自動模組定義為多專案的一部分,否則該多專案會定義真實模組(例如,作為另一個程式庫的適配器)。雖然這在 Gradle 建置中運作良好,但目前 IDEA/Eclipse 無法正確識別此類自動模組專案。您可以透過手動將為自動模組建置的 Jar 新增到在 IDE UI 中找不到它的專案的相依性來解決此問題。 ===

編譯時使用類別而非 jar

java-library 外掛程式的一項功能是,使用程式庫的專案僅需要類別資料夾進行編譯,而不需要完整的 JAR。這使得專案間的相依性更輕,因為僅在開發期間執行 Java 程式碼編譯時,不再執行資源處理 (processResources 任務) 和封存建構 (jar 任務)。

使用類別輸出而非 JAR 與否是消費者的決定。例如,Groovy 消費者將請求類別已處理的資源,因為在執行 AST 轉換作為編譯過程的一部分時可能需要這些資源。

消費者記憶體用量增加

間接的後果是,最新檢查將需要更多記憶體,因為 Gradle 將快照個別類別檔案而不是單個 jar。這可能會導致大型專案的記憶體消耗增加,其好處是在更多情況下 compileJava 任務是最新的(例如,變更資源不再變更上游專案的 compileJava 任務的輸入)

Windows 上巨型多專案的建置效能顯著下降

個別類別檔案快照的另一個副作用是,僅影響 Windows 系統,當在編譯類別路徑上處理大量類別檔案時,效能可能會顯著下降。這僅關乎非常大型的多專案,其中透過使用許多 api 相依性,類別路徑上存在大量類別。為了減輕這種情況,您可以將 org.gradle.java.compile-classpath-packaging 系統屬性設定為 true,以變更 Java 程式庫外掛程式的行為,使其針對編譯類別路徑上的所有內容使用 jar 而非類別資料夾。請注意,由於這具有其他效能影響和潛在的副作用(透過在編譯時期觸發所有 jar 任務),因此僅建議在您在 Windows 上遇到所述效能問題時才啟用此功能。

發佈程式庫

除了將程式庫發佈到元件儲存庫之外,您有時可能需要將程式庫及其相依性封裝在發佈交付物中。Java 程式庫發佈外掛程式旨在協助您完成此操作。