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 中使用的 scope 区分 api 和 implementation。这意味着编译类路径仅包含 Maven compile scope 的依赖项,而运行时类路径也添加了 Maven runtime scope 的依赖项。

这对于使用 Maven 发布的模块通常没有影响,因为定义项目的 POM 直接作为元数据发布。在那里,compile scope 包括编译项目所需的依赖项(即 implementation 依赖项)和编译针对已发布库所需的依赖项(即 API 依赖项)。对于大多数已发布的库,这意味着所有依赖项都属于 compile scope。如果你遇到现有库的此类问题,可以考虑使用组件元数据规则来修复构建中不正确的元数据。然而,如上所述,如果库是使用 Gradle 发布的,生成的 POM 文件仅将 api 依赖项放入 compile scope,并将剩余的 implementation 依赖项放入 runtime scope。

如果你的构建使用带有 Ivy 元数据的模块,如果所有模块都遵循特定结构,你可能会按此处所述激活 api 和 implementation 分离。

从 Gradle 5.0+ 开始,默认启用模块的 compile 和 runtime scope 分离。在 Gradle 4.6+ 中,你需要在 settings.gradle 中添加 enableFeaturePreview('IMPROVED_POM_SUPPORT') 来激活它。

识别 API 和实现依赖项

本节将通过简单的经验法则帮助你识别代码中的 API 和实现依赖项。第一条经验法则是

  • 在可能的情况下,优先使用 implementation 配置而不是 api

这会使依赖项远离使用者的编译类路径。此外,如果任何实现类型意外地泄露到公共 API 中,使用者将立即编译失败。

那么何时应该使用 api 配置呢?API 依赖项是包含至少一个在库二进制接口(通常称为其 ABI (Application Binary Interface))中暴露的类型的依赖项。这包括但不限于

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

  • 在公共方法参数中使用的类型,包括泛型参数类型(其中 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。请注意,HttpGetHttpEntity 用于 private 方法的签名中,因此它们不会导致 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

声明仅编译依赖项

这是你声明编译时需要但在运行时不需要的依赖项的地方。这通常包括在运行时发现时会被 shaded 的依赖项。

compileOnlyApi

声明仅编译 API 依赖项

这是你声明你的模块和使用者在编译时需要但在运行时不需要的依赖项的地方。这通常包括在运行时发现时会被 shaded 的依赖项。

runtimeOnly

声明运行时依赖项

这是你声明仅在运行时需要而编译时不需要的依赖项的地方。

testImplementation

测试依赖项

这是你声明用于编译测试的依赖项的地方。

testCompileOnly

声明仅测试编译依赖项

这是你声明仅在测试编译时需要但不应泄露到运行时的依赖项的地方。这通常包括在运行时发现时会被 shaded 的依赖项。

testRuntimeOnly

声明测试运行时依赖项

这是你声明仅在测试运行时需要而测试编译时不需要的依赖项的地方。

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

apiElements

用于编译此库

此配置供使用者使用,以检索编译此库所需的所有元素。

runtimeElements

用于执行此库

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

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

compileClasspath

用于编译此库

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

runtimeClasspath

用于执行此库

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

testCompileClasspath

用于编译此库的测试

此配置包含此库的测试编译类路径。

testRuntimeClasspath

用于执行此库的测试

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

为 Java Module System 构建 Modules

自 Java 9 起,Java 本身提供了模块系统,允许在编译和运行时进行严格封装。你可以通过在 main/java 源文件夹中创建 module-info.java 文件,将 Java 库转换为 Java Module

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 manifest 属性,如下文所述。)

  • 我们模块依赖的 Jar 本身就是一个模块,Gradle 根据 Jar 中是否存在 module-info.class(模块描述符的编译版本)来决定。(或者,替代方案是 Jar manifest 中是否存在 Automatic-Module-Name 属性)

下文描述了定义 Java 模块以及它如何与 Gradle 的依赖项管理交互的更多详细信息。你还可以查看一个现成的示例,直接试用 Java Module 支持。

声明模块依赖项

你在构建文件中声明的依赖项与你在 module-info.java 文件中声明的模块依赖项之间存在直接关系。理想情况下,声明应该保持同步,如下表所示。

表 4. Java 模块指令与 Gradle 依赖项声明配置的映射
Java 模块指令 Gradle 配置 目的

requires

implementation

声明实现依赖项

requires transitive

api

声明 API 依赖项

requires static

compileOnly

声明仅编译依赖项

requires static transitive

compileOnlyApi

声明仅编译 API 依赖项

Gradle 目前不会自动检查依赖项声明是否同步。这可能会在未来版本中添加。

有关声明模块依赖项的更多详细信息,请参阅Java Module System 文档

声明包可见性和服务

Java 模块系统支持比 Gradle 目前本身更细粒度的封装概念。例如,你需要明确声明哪些包是你 API 的一部分,哪些仅在你的模块内部可见。其中一些能力可能会在未来版本的 Gradle 中添加。目前,请参阅Java Module System 文档,了解如何在 Java Modules 中使用这些功能。

声明模块版本

Java Modules 也有一个版本,它在 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 中的开源库。一些库在其新版本中已经是带有模块描述符的完整模块。例如,com.google.code.gson:gson:2.8.9 的模块名称是 com.google.gson

其他库,例如 org.apache.commons:commons-lang3:3.10,可能不提供完整的模块描述符,但至少会在其 manifest 文件中包含一个 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
}

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

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

禁用 Java Module 支持

在极少数情况下,你可能希望禁用内置的 Java Module 支持,并通过其他方式定义模块路径。为了实现这一点,你可以禁用自动将任何 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 上的开源库现在都已完成)。

  • 如前一节所述,自动模块可用作你的真实模块和类路径上的传统库之间的适配器。

要将普通 Java 项目转换为自动模块,只需添加带有模块名称的 manifest 条目

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 中找不到它的项目的依赖项来解决此问题。 ===

使用 classes 代替 jar 进行编译

java-library 插件的一个特性是,使用该库的项目在编译时仅需要 classes 文件夹,而不是完整的 JAR。这使得项目间依赖关系更轻量,因为在开发过程中仅执行 Java 代码编译时,不再执行资源处理(processResources 任务)和归档构建(jar 任务)。

使用 classes 输出而不是 JAR 是由使用者决定的。例如,Groovy 使用者将请求 classes 已处理的资源,因为这些可能是在编译过程中执行 AST 转换所必需的。

使用者内存使用量增加

一个间接后果是,up-to-date 检查将需要更多内存,因为 Gradle 将对单个类文件而不是单个 jar 进行快照。这可能导致大型项目内存消耗增加,但好处是在更多情况下 compileJava 任务是 up-to-date 的(例如,更改资源不再更改上游项目的 compileJava 任务的输入)

Windows 上巨型多项目的构建性能显著下降

对单个类文件进行快照的另一个副作用,仅影响 Windows 系统,是在编译类路径上处理大量类文件时,性能可能会显著下降。这仅涉及非常大的多项目,其中通过使用许多 api 依赖项,类路径上存在大量类文件。为了缓解这个问题,你可以将 org.gradle.java.compile-classpath-packaging 系统属性设置为 true,以更改 Java 库插件的行为,使其在编译类路径上对所有内容使用 jars 而不是 class 文件夹。请注意,由于这会带来其他性能影响和潜在副作用(通过在编译时触发所有 jar 任务),因此仅建议在你遇到所述的 Windows 性能问题时激活此设置。

分发库

除了将库发布到组件仓库之外,有时你可能还需要将库及其依赖项打包为分发成果。 Java 库分发插件就是为此目的而生的。