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 组件。这是多项目构建中非常常见的用例,而且只要您有外部依赖项也是如此。

该插件公开了两个可用于声明依赖项的配置api:和implementation。配置api应该用于声明库 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 文件,这些文件准确区分针对库进行编译所需的内容和运行时使用库所需的内容(换句话说,不要混合编译库本身所需的内容以及针对库进行编译所需的内容)。

Gradle 7.0 中已删除compile和 配置。runtime请参阅升级指南如何迁移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+ 中,您需要通过添加enableFeaturePreview('IMPROVED_POM_SUPPORT')settings.gradle激活它。

识别 API 和实现依赖性

本节将帮助您使用简单的经验法则识别代码中的 API 和实现依赖关系。其中第一个是:

  • 尽可能优先选择implementation配置api

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

那么什么时候应该使用该api配置呢? API 依赖项是一种至少包含一种在库二进制接口中公开的类型的依赖项,通常称为 ABI(应用程序二进制接口)。这包括但不限于:

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

  • 公共方法参数中使用的类型,包括泛型参数类型(其中public是编译器可见的东西。即Java 世界中的publicprotected包私有成员)

  • 公共领域使用的类型

  • 公共注释类型

相比之下,以下列表中使用的任何类型都与 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();
    }
}

公共构造函数作为参数HttpClientWrapper使用HttpClient,因此暴露给消费者,因此属于API。请注意,HttpGetHttpEntity用于私有方法的签名,因此它们不会计入使 HttpClient 成为 API 依赖项。

另一方面,ExceptionUtils来自commons-lang库的类型仅在方法主体中使用(不在其签名中),因此它是实现依赖项。

因此,我们可以推断出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 库忽略已弃用的 main
  • 绿色的配置是用户应该用来声明依赖项的配置

  • 粉红色的配置是组件编译或针对库运行时使用的配置

  • 蓝色的配置是组件内部的,供其自己使用

下图描述了测试配置设置:

java 库忽略已弃用的测试

下表描述了每个配置的作用:

表 1. Java 库插件 - 用于声明依赖项的配置
配置名称 角色 消耗品? 可以解决吗? 描述

api

声明 API 依赖项

您可以在此处声明依赖项,这些依赖项会在编译时和运行时传递给使用者。

implementation

声明实现依赖关系

在这里您可以声明纯粹内部的依赖项,并不意味着暴露给消费者(它们仍然在运行时暴露给消费者)。

compileOnly

声明仅编译依赖项

您可以在此处声明编译时所需的依赖项,但运行时不需要。这通常包括在运行时发现时被隐藏的依赖项。

compileOnlyApi

声明仅编译 API 依赖项

您可以在此处声明模块和使用者在编译时所需的依赖项,但在运行时不需要。这通常包括在运行时发现时被隐藏的依赖项。

runtimeOnly

声明运行时依赖

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

testImplementation

测试依赖关系

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

testCompileOnly

声明仅测试编译依赖项

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

testRuntimeOnly

声明测试运行时依赖项

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

表 2. Java 库插件 — 使用者使用的配置
配置名称 角色 消耗品? 可以解决吗? 描述

apiElements

用于针对该库进行编译

是的

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

runtimeElements

用于执行该库

是的

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

表 3. Java 库插件 - 库本身使用的配置
配置名称 角色 消耗品? 可以解决吗? 描述

编译类路径

用于编译这个库

是的

该配置包含该库的编译类路径,因此在调用java编译器对其进行编译时使用。

运行时类路径

用于执行该库

是的

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

测试编译类路径

用于编译该库的测试

是的

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

测试运行时类路径

用于执行该库的测试

是的

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

为 Java 模块系统构建模块

从Java 9开始,Java本身提供了一个模块系统,允许在编译和运行时进行严格的封装。您可以通过在源文件夹中创建文件将 Java 库转换为Java 模块module-info.javamain/java

src
└── main
    └── java
        └── module-info.java

在模块信息文件中,您声明模块名称、要导出的模块的哪些包以及需要哪些其他模块。

模块信息.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-NameJar 清单属性,如下所述。)

  • 我们的模块所依赖的 Jar 本身就是一个模块,Gradles 根据module-info.class Jar 中是否存在 a(模块描述符的编译版本)来决定该模块。 (或者, Automatic-Module-NameJar 清单中是否存在某个属性)

下面,描述了有关定义 Java 模块以及如何与 Gradle 的依赖项管理交互的更多细节。您还可以查看现成的示例来直接尝试 Java 模块支持。

声明模块依赖关系

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

表 4. Java 模块指令和 Gradle 配置之间的映射以声明依赖项
Java 模块指令 等级配置 目的

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.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
}

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

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

禁用 Java 模块支持

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

要实现此功能,您需要modularity.inferModulePath = false对 Java 扩展(针对所有任务)或单个任务进行设置。

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

分发库

除了将库发布到组件存储库之外,有时您可能还需要将库及其依赖项打包到分发可交付成果中。 Java库分发插件可以帮助您做到这一点。