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 库插件都通过 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 世界中的 publicprotectedpackage private 成员)

  • 在公共字段中使用的类型

  • 公共注释类型

相比之下,以下列表中使用的任何类型与 ABI 无关,因此应声明为 implementation 依赖项

  • 仅在方法主体中使用的类型

  • 仅在私有成员中使用的类型

  • 仅在内部类中找到的类型(Gradle 的未来版本将允许您声明哪些包属于公共 API)

以下类使用几个第三方库,其中一个在类的公共 API 中公开,另一个仅在内部使用。导入语句无法帮助我们确定哪个是哪个,因此我们必须查看字段、构造函数和方法

示例:区分 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 库插件 - 用于声明依赖项的配置
配置名称 作用 可消耗? 可解析? 描述

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.gsoncom.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 将类路径视为一个模块(即所谓的未命名模块)。

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 Library 插件的行为,以便在编译类路径上的所有内容中使用 jar 而不是类文件夹。请注意,由于这会产生其他性能影响和潜在的副作用,因此通过在编译时触发所有 jar 任务,只有在 Windows 上遇到所述性能问题时才建议激活此功能。

分发库

除了将库发布到组件存储库之外,有时您可能需要将库及其依赖项打包到可交付的分发包中。 Java Library Distribution 插件 可以帮助您完成此操作。