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 和实现分离。 这意味着编译类路径仅包含 Maven compile
作用域的依赖项,而运行时类路径也添加了 Maven runtime
作用域的依赖项。
这通常对使用 Maven 发布的模块没有影响,在 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(应用程序二进制接口)。 这包括但不限于
-
超类或接口中使用的类型
-
公共方法参数中使用的类型,包括泛型参数类型(其中公共是编译器可见的内容。即,Java 世界中的公共、受保护和包私有成员)
-
公共字段中使用的类型
-
公共注解类型
相比之下,以下列表中使用的任何类型都与 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 本身就是一个模块,Gradles 根据 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
依赖项,类路径上存在大量类。 为了缓解这种情况,您可以将 org.gradle.java.compile-classpath-packaging
系统属性设置为 true
,以更改 Java 库插件的行为,从而对编译类路径上的所有内容使用 jar 而不是类文件夹。 请注意,由于这会产生其他性能影响和潜在的副作用,例如在编译时触发所有 jar 任务,因此仅当您在 Windows 上遇到所述性能问题时才建议激活此功能。
分发库
除了发布库到组件仓库之外,您有时可能需要将库及其依赖项打包到分发交付件中。 Java 库分发插件旨在帮助您做到这一点。