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 库插件都通过 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
这会将依赖项从使用者的编译类路径中移除。此外,如果任何实现类型意外泄露到公共 API 中,使用者将立即无法编译。
那么您应该何时使用 api
配置?API 依赖项包含至少一个在库二进制接口(通常称为其 ABI(应用程序二进制接口))中公开的类型。这包括但不限于
-
在超类或接口中使用的类型
-
在公共方法参数中使用的类型,包括泛型参数类型(其中 public 是编译器可见的内容。即,Java 世界中的 public、protected 和 package private 成员)
-
在公共字段中使用的类型
-
公共注释类型
相比之下,以下列表中使用的任何类型与 ABI 无关,因此应声明为 implementation
依赖项
-
仅在方法主体中使用的类型
-
仅在私有成员中使用的类型
-
仅在内部类中找到的类型(Gradle 的未来版本将允许您声明哪些包属于公共 API)
以下类使用几个第三方库,其中一个在类的公共 API 中公开,另一个仅在内部使用。导入语句无法帮助我们确定哪个是哪个,因此我们必须查看字段、构造函数和方法
示例:区分 API 和实现
// 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。请注意,HttpGet
和 HttpEntity
用于私有方法的签名中,因此它们不会计入使 HttpClient 成为 API 依赖项。
另一方面,来自 commons-lang
库的 ExceptionUtils
类型仅在方法体中使用(不在其签名中),因此它是一个实现依赖项。
因此,我们可以推断 httpclient
是一个 API 依赖项,而 commons-lang
是一个实现依赖项。此结论转化为构建脚本中的以下声明
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.gson
的 com.google.code.gson:gson:2.8.9
。
其他库,例如 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 Library 插件的行为,以便在编译类路径上的所有内容中使用 jar 而不是类文件夹。请注意,由于这会产生其他性能影响和潜在的副作用,因此通过在编译时触发所有 jar 任务,只有在 Windows 上遇到所述性能问题时才建议激活此功能。
分发库
除了将库发布到组件存储库之外,有时您可能需要将库及其依赖项打包到可交付的分发包中。 Java Library Distribution 插件 可以帮助您完成此操作。