Java 函式庫外掛擴充了 Java 外掛 (java) 的功能,提供了關於 Java 函式庫的特定知識。特別是,Java 函式庫會向消費者 (例如,使用 Java 或 Java 函式庫外掛的其他專案) 公開 API。使用此外掛時,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 Library 外掛程式都會透過 POM 中使用的範圍來尊重 api 和 implementation 的區分。這表示編譯類別路徑只包含 Maven compile 範圍的相依性,而執行時期類別路徑也會新增 Maven runtime 範圍的相依性。

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

如果您的建置使用 Ivy 元資料的模組,您或許可以依照 這裡 所述,在所有模組都遵循特定結構的情況下,啟用 api 和 implementation 的區分。

在 Gradle 5.0+ 中,預設會將模組的編譯和執行時間範圍分開。在 Gradle 4.6+ 中,您需要在 settings.gradle 中加入 enableFeaturePreview('IMPROVED_POM_SUPPORT') 來啟用此功能。

辨識 API 和 implementation 相依性

本節將協助您使用簡單的經驗法則來辨識程式碼中的 API 和 Implementation 相依性。第一個經驗法則為

  • 盡可能優先使用 implementation 組態而非 api

這會讓相依性遠離使用者的編譯類別路徑。此外,如果任何 implementation 類型意外外洩到公開 API 中,使用者會立即編譯失敗。

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

  • 用於超級類別或介面的類型

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

  • 用於公開欄位的類型

  • 公開註解類型

相對地,下列清單中使用的任何類型都與 ABI 無關,因此應宣告為 implementation 相依性

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

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

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

下列類別使用兩個第三方函式庫,其中一個在類別的公開 API 中公開,另一個僅在內部使用。匯入陳述式無法幫助我們判斷哪一個是哪一個,因此我們必須查看欄位、建構函式和方法

範例:辨別 API 和 implementation 的差異

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

HttpClientWrapperpublic 建構函式將 HttpClient 用作參數,因此會公開給使用者,因此屬於 API。請注意,HttpGetHttpEntity 用於 private 方法的簽章中,因此不會計算在讓 HttpClient 成為 API 相依性的因素中。

另一方面,來自 commons-lang 函式庫的 ExceptionUtils 類型僅用於方法主體(不在其簽章中),因此是 implementation 相依性。

因此,我們可以推論 httpclient 是 API 相依性,而 commons-lang 是 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'
}

Java 函式庫外掛程式組態

當 Java 函式庫外掛程式使用中,下圖說明組態設定方式。

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

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

  • 藍色 的組態是元件內部專用的內部組態

下一個圖表說明測試組態設定

java library ignore deprecated test

下表說明每個組態的角色

表格 1. Java 函式庫外掛程式 - 宣告相依性時使用的組態
組態名稱 角色 可消耗? 可解析? 說明

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 或(已棄用)compile 相依性。若要減輕這個問題,您可以將 org.gradle.java.compile-classpath-packaging 系統屬性設定為 true,以變更 Java 函式庫外掛程式的行為,讓它在編譯類別路徑上的所有項目中使用 jar 而不是類別資料夾。請注意,由於這會對效能和其他方面造成影響,並觸發編譯時間的所有 jar 任務,因此只有在 Windows 上遇到所述效能問題時,才建議啟用此功能。

散布函式庫

除了將發佈函式庫到元件儲存庫之外,有時您可能需要將函式庫及其相依性封裝在可配送的發行版中。Java 函式庫發行版外掛程式就是為此而生。