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 和实现分离。这意味着编译类路径只包含 Maven compile 作用域的依赖项,而运行时类路径还会添加 Maven runtime 作用域的依赖项。

这通常对使用 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(应用程序二进制接口)。这包括但不限于

  • 在超类或接口中使用的类型

  • 在公共方法参数中使用的类型,包括泛型参数类型(其中 public 是对编译器可见的内容。即,Java 世界中的 publicprotectedpackage private 成员)

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

  • 公共注解类型

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

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

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

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

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

示例:区分 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();
    }
}

HttpClientWrapperpublic 构造函数使用 HttpClient 作为参数,因此它暴露给消费者并因此属于 API。请注意,HttpGetHttpEntityprivate 方法的签名中使用,因此它们不构成将 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 库插件 - 用于声明依赖项的配置
配置名称 作用 可消费? 可解析? 描述

annotationProcessor

声明注解处理器

此配置用于声明注解处理器,确保它们在编译阶段可用于代码生成。

api

声明 API 依赖项

您在此处声明以传递方式导出到消费者的依赖项,用于编译时和运行时。

implementation

声明实现依赖项

您在此处声明纯内部且不打算暴露给消费者的依赖项(它们仍在运行时暴露给消费者)。

compileOnly

声明仅编译依赖项

您在此处声明编译时需要但在运行时不需要的依赖项。这通常包括运行时发现时被遮蔽的依赖项。

compileOnlyApi

声明仅编译 API 依赖项

您在此处声明模块和消费者在编译时需要但在运行时不需要的依赖项。这通常包括运行时发现时被遮蔽的依赖项。

runtimeOnly

声明运行时依赖项

您在此处声明仅在运行时需要,而不是在编译时需要的依赖项。

testImplementation

测试依赖项

您在此处声明用于编译测试的依赖项。

testCompileOnly

声明仅测试编译依赖项

您在此处声明仅在测试编译时需要,但不应泄露到运行时的依赖项。这通常包括运行时发现时被遮蔽的依赖项。

testRuntimeOnly

声明测试运行时依赖项

您在此处声明仅在测试运行时需要,而不是在测试编译时需要的依赖项。

表 2. Java 库插件 — 消费者使用的配置
配置名称 作用 可消费? 可解析? 描述

apiElements

用于针对此库进行编译

此配置旨在供消费者使用,以检索针对此库进行编译所需的所有元素。

runtimeElements

用于执行此库

此配置旨在供消费者使用,以检索运行此库所需的所有元素。

表 3. Java 库插件 - 库本身使用的配置
配置名称 作用 可消费? 可解析? 描述

compileClasspath

用于编译此库

此配置包含此库的编译类路径,因此在调用 Java 编译器编译它时使用。

运行时类路径

用于执行此库

此配置包含此库的运行时类路径

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.13.1")      // 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.13.1'      // 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
}

虽然真正的模块不能直接依赖未命名模块(只能通过添加命令行标志),但自动模块也可以看到未命名模块。因此,如果您无法避免依赖没有模块信息的库,则可以将该库包装在自动模块中作为项目的一部分。下一节将介绍如何操作。

处理非模块的另一种方法是使用artifact transforms自行丰富现有 Jar 的模块描述符。此示例包含一个小的 buildSrc 插件,用于注册此类转换,您可以根据需要使用和调整。如果您想构建一个完全模块化应用程序并希望 Java 运行时将所有内容都视为真正的模块,这可能会很有趣。

禁用 Java 模块支持

在极少数情况下,您可能希望禁用内置的 Java 模块支持,并通过其他方式定义模块路径。为此,您可以禁用自动将任何 Jar 放在模块路径上的功能。然后 Gradle 会将带有模块信息的 Jar 放在类路径上,即使您的源集中有 module-info.java。这与 Gradle <7.0 版本的行为相对应。

要使其正常工作,您需要将 Java 扩展 (针对所有任务) 或单个任务上的 modularity.inferModulePath = false 设置为 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 依赖项,类路径上存在大量类。为了缓解这个问题,您可以将 org.gradle.java.compile-classpath-packaging 系统属性设置为 true,以更改 Java 库插件的行为,使其在编译类路径上的所有内容都使用 jar 而不是类文件夹。请注意,由于这会产生其他性能影响和潜在副作用,因为它会在编译时触发所有 jar 任务,因此仅当您在 Windows 上遇到所述的性能问题时才建议激活此功能。

分发库

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