Java库插件扩展的功能的Java插件,通过提供有关Java库的具体知识. 特别是,Java库向使用者(即使用Java或Java库插件的其他项目)公开API. 使用此插件时,Java插件公开的所有源集,任务和配置都是隐式可用的.

Usage

要使用Java库插件,请在构建脚本中包括以下内容:

示例1.使用Java库插件
build.gradle
plugins {
    id 'java-library'
}
build.gradle.kts
plugins {
    `java-library`
}

API and implementation separation

标准Java插件和Java库插件之间的主要区别在于,后者引入了向消费者公开的API的概念. 库是一个Java组件,打算由其他组件使用. 在多项目构建中,这是一个非常常见的用例,但在您具有外部依赖关系时也是如此.

该插件暴露了两个配置 ,可以用来声明依赖性: apiimplementation . api配置应用于声明由库API导出的依赖关系,而implementation配置应用于声明组件内部的依赖关系.

例子2.声明API和实现依赖
build.gradle
dependencies {
    api 'org.apache.httpcomponents:httpclient:4.5.7'
    implementation 'org.apache.commons:commons-lang3:3.5'
}
build.gradle.kts
dependencies {
    api("org.apache.httpcomponents:httpclient:4.5.7")
    implementation("org.apache.commons:commons-lang3:3.5")
}

api配置中出现的依赖项将传递给库的使用者,并因此而出现在使用者的编译类路径上. 另一方面,在implementation配置中找到的依赖项不会暴露给使用者,因此不会泄漏到使用者的编译类路径中. 这有几个好处:

  • 依赖项不会再泄漏到使用者的编译类路径中,因此您永远不会意外地依赖于传递性依赖项

  • 减少类路径大小,加快了编译速度

  • 实施依赖项发生更改时,重新编译次数更少:无需重新编译使用者

  • 更清洁的发布:与新的maven-publish插件一起使用时,Java库生成的POM文件可准确区分针对该库进行编译所需的内容和在运行时使用该库所需的内容(换句话说,不要混合编译库本身所需的内容和对库进行编译所需的内容).

compile配置仍然存在,但不应使用,因为它不能提供apiimplementation配置所提供的保证.

如果您的构建使用带有POM元数据的已发布模块,则Java和Java库插件会通过pom中使用的作用域来实现api和实现分离. 这意味着编译类路径仅包括compile范围的依赖关系,而运行时类路径也添加了runtime范围的依赖关系.

这通常对用Maven发布的模块没有影响,在Maven中,定义项目的POM直接作为元数据发布. 在那里,编译范围既包括编译项目所需的依赖关系(即实现依赖关系),又包括针对已发布库进行编译所需的依赖关系(即API依赖关系). 对于大多数已发布的库,这意味着所有依赖项都属于编译范围. 如果您在现有库中遇到此类问题,则可以考虑使用组件元数据规则来修复构建中不正确的元数据. 但是,如上所述,如果该库与Gradle一起发布,则生成的POM文件仅将api依赖项放入编译范围,而将其余implementation依赖项放入运行时范围.

如果您的构建使用带有Ivy元数据的模块,则如果所有模块都遵循特定的结构,则可以按此处所述激活api和实现分离.

在Gradle 5.0+中,默认情况下,将模块的编译和运行时范围分开是活动的. 在Gradle 4.6+中,您需要通过在settings.gradle中添加enableFeaturePreview('IMPROVED_POM_SUPPORT')来激活它.

Recognizing API and implementation dependencies

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

  • 尽可能将implementation配置apiapi

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

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

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

  • 公共方法参数中使用的类型,包括通用参数类型(其中public是编译器可见的东西.即Java世界中的publicprotectedpackage private成员)

  • 公共领域中使用的类型

  • 公开注释类型

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

  • 方法主体中专门使用的类型

  • 专用于私人会员的类型

  • 内部类专有的类型(将来的Gradle版本将允许您声明哪些包属于公共API)

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

Example: Making the difference between API and implementation

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是实现依赖关系. 该结论转化为构建脚本中的以下声明:

例子3.声明API和实现依赖
build.gradle
dependencies {
    api 'org.apache.httpcomponents:httpclient:4.5.7'
    implementation 'org.apache.commons:commons-lang3:3.5'
}
build.gradle.kts
dependencies {
    api("org.apache.httpcomponents:httpclient:4.5.7")
    implementation("org.apache.commons:commons-lang3:3.5")
}

The Java Library plugin configurations

下图描述了使用Java库插件时的主要配置设置.

java library ignore deprecated main
  • 绿色的配置是用户应用来声明依赖项的配置

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

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

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

java library ignore deprecated test

从Java插件继承的compiletestCompileruntimetestRuntime配置仍然可用,但已弃用. 您应避免使用它们,因为仅保留它们是为了向后兼容.

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

表1. Java库插件-用于声明依赖关系的配置
配置名称 Role Consumable? Resolvable? Description

api

声明API依赖项

no

no

在这里,您应该声明依赖关系,这些依赖关系将传递给使用者,以进行编译.

implementation

声明实现依赖

no

no

在这里,您应该声明依赖关系,这些依赖关系完全是内部的,并且不打算向消费者公开.

compileOnly

声明仅编译依赖项

no

no

在这里,您应该声明仅在编译时需要的依赖项,而不应泄漏到运行时. 这通常包括在运行时找到时会被阴影化的依赖项.

runtimeOnly

声明运行时依赖项

no

no

在这里,您应该声明仅在运行时才需要的依赖项,而在编译时则不需要.

testImplementation

测试依赖

no

no

在这里,您应该声明用于编译测试的依赖项.

testCompileOnly

声明测试仅编译依赖项

no

no

在这里,您应该声明仅在测试编译时需要的依赖项,而不应泄漏到运行时. 这通常包括在运行时找到时会被阴影化的依赖项.

testRuntimeOnly

声明测试运行时依赖项

no

no

在这里,您应该声明仅在测试运行时才需要的依赖项,而在测试编译时则不需要.

表2. Java库插件—使用者使用的配置
配置名称 Role Consumable? Resolvable? Description

apiElements

用于针对该库进行编译

yes

no

此配置供使用者使用,以检索对该库进行编译所需的所有元素. 与default配置不同,这不会泄漏实现或运行时依赖项.

runtimeElements

用于执行此库

yes

no

使用者将使用此配置来检索对该库运行所需的所有元素.

表3. Java库插件-库本身使用的配置
配置名称 Role Consumable? Resolvable? Description

compileClasspath

用于编译该库

no

yes

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

runtimeClasspath

用于执行此库

no

yes

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

testCompileClasspath

For compiling the tests of this library

no

yes

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

testRuntimeClasspath

用于执行此库的测试

no

yes

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

Building Modules for the Java Module System

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

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

在模块信息文件中,声明模块名称 ,要导出的模块包以及所需的其他模块.

module-info.java文件
module org.gradle.sample {
    exports org.gradle.sample;
    requires com.google.gson;
}

为了告诉Java编译器Jar是一个模块,而不是传统的Java库,Gradle需要将其放置在所谓的模块路径上 . 它是classpath的替代方法,它是告诉编译器已编译依赖项的传统方式. 如果满足以下三个条件,则Gradle会自动将您的依赖项Jar放在模块路径上,而不是在类路径上:

  • 模块路径推断是通过java. modularity.inferModulePath.set(true)打开的java. modularity.inferModulePath.set(true) java. modularity.inferModulePath.set(true)

  • 实际上,我们正在构建一个模块(与传统库相反),通过添加module-info.java文件来表示该module-info.java . (另一个选项是添加" Automatic-Module-Name Jar清单"属性,如下面所述 .)

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

在下文中,将描述有关定义Java模块以及如何与Gradle的依赖管理进行交互的更多详细信息. 您还可以查看一个现成的示例 ,直接尝试Java模块支持.

Java Module System支持是一个孵化功能,因此您需要显式打开模块路径推断 ,如下所示.

例子4.激活模块路径推断
build.gradle
java {
    modularity.inferModulePath = true
}
build.gradle.kts
java {
    modularity.inferModulePath.set(true)
}

Declaring module dependencies

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

表4. Java模块指令和Gradle配置之间的映射以声明依赖关系
Java模块指令 摇篮配置 Purpose

requires

implementation

声明实现依赖

requires transitive

api

声明API依赖项

requires static

compileOnly

声明仅编译依赖项

Gradle当前不会自动检查依赖项声明是否同步. 这可能会在将来的版本中添加.

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

Declaring package visibility and services

与Gradle本身相比,Java模块系统支持更多更精细的颗粒封装概念. 例如,您明确需要声明哪些包是API的一部分,哪些仅在模块内部可见. 其中一些功能可能会在将来的版本中添加到Gradle本身. 现在,请参阅Java模块系统上的文档以了解如何在Java模块中使用这些功能.

Declaring module versions

Java模块还具有一个版本,该版本被编码为module-info.class文件中模块标识的一部分. 运行模块时可以检查此版本.

例子5.在构建脚本中声明模块版本,或者直接作为编译任务选项
build.gradle
version = '1.2'

tasks.compileJava {
    // use the project's version or define one directly
    options.javaModuleVersion = provider { project.version }
}
build.gradle.kts
version = "1.2"

tasks.compileJava {
    // use the project's version or define one directly
    options.javaModuleVersion.set(provider { project.version as String })
}

Using libraries that are not modules

您可能想在模块化Java项目中使用外部库,例如Maven Central的OSS库. 某些较新版本的库已经是带有模块描述符的完整模块. 例如,模块名称为com.google.gson com.google.code.gson:gson:2.8.6 .

其他的,例如org.apache.commons:commons-lang3:3.10 ,可能没有提供完整的模块描述符,但至少会在清单文件中包含一个Automatic-Module-Name条目来定义模块的名称( org.apache.commons.lang3中的org.apache.commons.lang3 ). 此类模块仅以模块描述的名称命名,称为自动模块 ,该模块导出所有软件包并可以读取模块路径上的所有模块.

第三种情况是传统的库,它们根本不提供模块信息,例如commons-cli:commons-cli:1.4 . Gradle将此类库放在类路径而不是模块路径上. 然后,Java将类路径视为一个模块(所谓的未命名模块).

例子6.对在构建文件中声明的模块和库的依赖
build.gradle
dependencies {
    implementation 'com.google.code.gson:gson:2.8.6'       // real module
    implementation 'org.apache.commons:commons-lang3:3.10' // automatic module
    implementation 'commons-cli:commons-cli:1.4'           // plain library
}
build.gradle.kts
dependencies {
    implementation("com.google.code.gson:gson:2.8.6")       // 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运行时将所有内容都视为一个真正的模块,那么这可能会很有趣.

Building an automatic module

如果可以,您应该始终为模块编写完整的module-info.java描述符. 不过,在某些情况下,您可能会考虑(最初)仅为自动模块提供模块名称

  • 您正在使用的不是模块的库,但是您希望在下一个发行版中使其可用. 添加Automatic-Module-Name是一个很好的第一步(到目前为止,Maven Central上最流行的OSS库已经做到了).

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

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

例子7.声明一个自动模块名称为Jar manifest属性
build.gradle
tasks.jar {
    manifest {
        attributes('Automatic-Module-Name': 'org.gradle.sample')
    }
}
build.gradle.kts
tasks.jar {
    manifest {
        attributes("Automatic-Module-Name" to "org.gradle.sample")
    }
}

您可以将自动模块定义为多项目的一部分,否则可以定义实际模块(例如,作为另一个库的适配器). 尽管这在Gradle构建中可以正常工作,但是IDEA / Eclipse目前无法正确识别此类自动模块项目. 您可以通过将为自动模块构建的Jar手动添加到在IDE的UI中找不到它的项目的依赖项中来解决.

Using classes instead of jar for compilation

java-library插件的一个功能是,使用该库的项目仅需要classes文件夹进行编译,而不需要完整的JAR. 当在开发过程中仅执行Java代码编译时,由于不再执行资源处理( processResources任务)和归档构建( jar任务),因此可以减轻项目间的依赖性.

班输出,而不是JAR中的使用与否是消费者决定. 例如,Groovy使用者将请求类和已处理资源,因为在编译过程中执行AST转换可能需要这些类和已处理资源.

Increased memory usage for consumers

间接的结果是,最新的检查将需要更多的内存,因为Gradle会快照单个类文件而不是单个jar. 这可能会导致大型项目的内存消耗增加,并具有在更多情况下使compileJava任务为最新状态的好处(例如,更改资源不再更改上游项目的compileJava任务的输入).

Significant build performance drop on Windows for huge multi-projects

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

Distributing a library

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