Java 函式庫外掛程式
Java 函式庫外掛擴充了 Java 外掛 (java
) 的功能,提供了關於 Java 函式庫的特定知識。特別是,Java 函式庫會向消費者 (例如,使用 Java 或 Java 函式庫外掛的其他專案) 公開 API。使用此外掛時,Java 外掛公開的所有來源組、工作和組態都會隱含地提供。
用法
若要使用 Java 函式庫外掛,請在組建指令碼中包含下列內容
plugins {
`java-library`
}
plugins {
id 'java-library'
}
API 和實作分離
標準 Java 外掛和 Java 函式庫外掛之間的主要差異在於,後者引入了公開給消費者的API 概念。函式庫是一個 Java 元件,旨在供其他元件使用。這是多專案組建中非常常見的用例,但只要您有外部相依性,就會使用它。
此外掛程式公開兩個 組態,可用於宣告相依性:api
和 implementation
。api
組態應使用於宣告由程式庫 API 匯出的相依性,而 implementation
組態應使用於宣告元件內部的相依性。
dependencies {
api("org.apache.httpcomponents:httpclient:4.5.7")
implementation("org.apache.commons:commons-lang3:3.5")
}
dependencies {
api 'org.apache.httpcomponents:httpclient:4.5.7'
implementation 'org.apache.commons:commons-lang3:3.5'
}
出現在 api
組態中的相依性會以遞移方式公開給程式庫的使用者,因此會出現在使用者的編譯類別路徑上。另一方面,在 implementation
組態中找到的相依性不會公開給使用者,因此不會洩漏到使用者的編譯類別路徑中。這帶來許多好處
-
相依性不再洩漏到使用者的編譯類別路徑中,因此您絕不會意外依賴遞移相依性
-
由於類別路徑大小縮小,編譯速度更快
-
實作相依性變更時重新編譯次數減少:使用者不需要重新編譯
-
更簡潔的發布:與新的
maven-publish
外掛程式搭配使用時,Java 程式庫會產生 POM 檔案,明確區分編譯程式庫所需項目和執行時期使用程式庫所需項目(換句話說,不要混淆編譯程式庫本身所需項目和編譯程式庫所需項目)。
compile 和 runtime 組態已在 Gradle 7.0 中移除。請參閱 升級指南,了解如何移轉到 implementation 和 api 組態。
|
如果您的建置使用已發布的模組和 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 世界中的 public、protected 和 package private 成員)
-
用於公開欄位的類型
-
公開註解類型
相對地,下列清單中使用的任何類型都與 ABI 無關,因此應宣告為 implementation
相依性
-
僅在方法主體中使用的類型
-
僅在私人成員中使用的類型
-
僅在內部類別中找到的類型(未來版本的 Gradle 會讓您宣告哪些套件屬於公開 API)
下列類別使用兩個第三方函式庫,其中一個在類別的公開 API 中公開,另一個僅在內部使用。匯入陳述式無法幫助我們判斷哪一個是哪一個,因此我們必須查看欄位、建構函式和方法
範例:辨別 API 和 implementation 的差異
// 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
的 public 建構函式將 HttpClient
用作參數,因此會公開給使用者,因此屬於 API。請注意,HttpGet
和 HttpEntity
用於 private 方法的簽章中,因此不會計算在讓 HttpClient 成為 API 相依性的因素中。
另一方面,來自 commons-lang
函式庫的 ExceptionUtils
類型僅用於方法主體(不在其簽章中),因此是 implementation 相依性。
因此,我們可以推論 httpclient
是 API 相依性,而 commons-lang
是 implementation 相依性。此結論會轉換成在建置指令碼中的下列宣告
dependencies {
api("org.apache.httpcomponents:httpclient:4.5.7")
implementation("org.apache.commons:commons-lang3:3.5")
}
dependencies {
api 'org.apache.httpcomponents:httpclient:4.5.7'
implementation 'org.apache.commons:commons-lang3:3.5'
}
Java 函式庫外掛程式組態
當 Java 函式庫外掛程式使用中,下圖說明組態設定方式。

-
綠色 的組態是使用者宣告相依性時應使用的組態
-
粉紅色 的組態是元件編譯或對函式庫執行時使用的組態
-
藍色 的組態是元件內部專用的內部組態
下一個圖表說明測試組態設定

下表說明每個組態的角色
組態名稱 | 角色 | 可消耗? | 可解析? | 說明 |
---|---|---|---|---|
|
宣告 API 相依性 |
否 |
否 |
在此宣告在編譯時間和執行時間傳遞匯出的相依性。 |
|
宣告實作相依性 |
否 |
否 |
在此宣告純粹內部且不打算公開給使用者(在執行時間仍會公開給使用者)的相依性。 |
|
宣告僅編譯相依性 |
否 |
否 |
在此宣告編譯時間需要,但執行時間不需要的相依性。這通常包括在執行時間找到時會遮蔽的相依性。 |
|
宣告僅編譯 API 相依性 |
否 |
否 |
在此宣告模組和使用者在編譯時間需要,但在執行時間不需要的相依性。這通常包括在執行時間找到時會遮蔽的相依性。 |
|
宣告執行時間相依性 |
否 |
否 |
在此宣告僅在執行時間需要,但在編譯時間不需要的相依性。 |
|
測試相依性 |
否 |
否 |
在此宣告用於編譯測試的相依性。 |
|
宣告僅測試編譯相依性 |
否 |
否 |
在此宣告僅在測試編譯時間需要,但不得外洩到執行時間的相依性。這通常包括在執行時間找到時會遮蔽的相依性。 |
|
宣告測試執行時間相依性 |
否 |
否 |
在此宣告僅在測試執行時間需要,但在測試編譯時間不需要的相依性。 |
組態名稱 | 角色 | 可消耗? | 可解析? | 說明 |
---|---|---|---|---|
|
用於編譯此函式庫 |
是 |
否 |
此設定是供使用者使用,用於擷取編譯此函式庫所需的所有元素。 |
|
用於執行此函式庫 |
是 |
否 |
此設定是供使用者使用,用於擷取執行此函式庫所需的所有元素。 |
組態名稱 | 角色 | 可消耗? | 可解析? | 說明 |
---|---|---|---|---|
compileClasspath |
用於編譯此函式庫 |
否 |
是 |
此設定包含此函式庫的編譯類別路徑,因此在呼叫 Java 編譯器來編譯函式庫時會使用此設定。 |
runtimeClasspath |
用於執行此函式庫 |
否 |
是 |
此設定包含此函式庫的執行時期類別路徑 |
testCompileClasspath |
用於編譯此函式庫的測試 |
否 |
是 |
此設定包含此函式庫的測試編譯類別路徑。 |
testRuntimeClasspath |
用於執行此函式庫的測試 |
否 |
是 |
此設定包含此函式庫的測試執行時期類別路徑 |
為 Java 模組系統建置模組
自 Java 9 以來,Java 本身提供了一個模組系統,可在編譯和執行期間進行嚴格封裝。您可以透過在 main/java
來源資料夾中建立 module-info.java
檔案,將 Java 函式庫轉換成Java 模組。
src
└── main
└── 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
檔案中宣告的模組相依性之間有直接關係。理想上,宣告應該同步,如下表所示。
Java 模組指令 | Gradle 組態 | 目的 |
---|---|---|
|
|
宣告實作相依性 |
|
|
宣告 API 相依性 |
|
|
宣告僅編譯相依性 |
|
|
宣告僅編譯 API 相依性 |
Gradle 目前不會自動檢查相依性宣告是否同步。這可能會新增到未來的版本中。
如需宣告模組相依性的更多詳細資訊,請參閱Java 模組系統文件。
宣告套件可見性和服務
Java 模組系統支援比 Gradle 本身目前支援的更精細的封裝概念。例如,您需要明確宣告哪些套件是 API 的一部分,哪些套件只在模組內部可見。其中一些功能可能會在未來的版本中新增到 Gradle 本身。目前,請參閱Java 模組系統文件,以了解如何在 Java 模組中使用這些功能。
宣告模組版本
Java 模組也有版本,編碼為 module-info.class
檔案中模組識別的一部分。可以在模組執行時檢查此版本。
version = "1.2"
tasks.compileJava {
// use the project's version or define one directly
options.javaModuleVersion = provider { version as String }
}
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 會將類別路徑視為一個模組(所謂的未命名模組)。
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
}
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 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
}
雖然真實模組無法直接相依於未命名模組(只能透過新增命令列旗標),但自動模組也可以看到未命名模組。因此,如果您無法避免依賴沒有模組資訊的函式庫,您可以將該函式庫包裝在自動模組中,作為專案的一部分。下一節將說明如何執行此操作。
停用 Java 模組支援
在少數情況下,您可能想要停用內建的 Java 模組支援,並透過其他方式定義模組路徑。為達成此目的,您可以停用自動將任何 Jar 放入模組路徑的功能。然後,即使您的來源組中有 module-info.java
,Gradle 仍會將具有模組資訊的 Jar 放入類別路徑。這與 Gradle 版本 <7.0 的行為相符。
為執行此操作,您需要在 Java 擴充功能(針對所有任務)或個別任務上設定 modularity.inferModulePath = false
。
java {
modularity.inferModulePath = false
}
tasks.compileJava {
modularity.inferModulePath = false
}
java {
modularity.inferModulePath = false
}
tasks.named('compileJava') {
modularity.inferModulePath = false
}
建置自動模組
如果您能,您應該總是為您的模組撰寫完整的 module-info.java
描述符。儘管如此,在某些情況下,您可能會考慮(最初)只為自動模組提供一個模組名稱
-
您正在開發一個不是模組的函式庫,但您希望在下次版本中將其作為模組使用。新增
Automatic-Module-Name
是個不錯的第一步(Maven central 上最受歡迎的 OSS 函式庫目前都已執行此操作)。 -
如前一節所述,自動模組可用作真實模組和類別路徑上傳統函式庫之間的轉接器。
若要將一般的 Java 專案轉換為自動模組,只要加入包含模組名稱的清單項目即可
tasks.jar {
manifest {
attributes("Automatic-Module-Name" to "org.gradle.sample")
}
}
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 函式庫發行版外掛程式就是為此而生。