Contents

此插件扩展了Java plugin 的能力 - 通过提供Java libraries的特定知识 ..
除此之外,一个Java library 暴露了一个API 到消费者(例如,其他项目使用这个Java 或者 Java Library 插件)
通过Java 插件暴露的所有的资源集,任务以及配置都是隐式可用的 - 当使用此插件时 …

Usage

为了使用此插件,增加以下内容到构建脚本中

  1. plugins {
  2. id 'java-library'
  3. }

API and implementation separation

Java Library 插件与标准的Java插件的完全不同 - 后者引入了向消费者公开的API的概念. 一个库的含义是Java 组件并且打算由其他组件消费 … 在多项目构建中尤为常见,但是你也可以有外部依赖 ..
这个插件暴露了两个configurations - 能够被用来声明依赖: api 以及 implementation.

  • api 配置应该被用来声明一些暴露的Library API … 表示它们可以被使用此依赖的组件消费
  • implementation 配置应该被用来声明当前组件内部使用的依赖 ..

    1. dependencies {
    2. api 'org.apache.httpcomponents:httpclient:4.5.7'
    3. implementation 'org.apache.commons:commons-lang3:3.5'
    4. }

    在api配置中出现的依赖将会传递性暴露给库的消费者 … 并且它们将出现在消费者的编译类路径上..
    在implementation配置中发现的依赖将不会暴露,因此它们不会出现在消费者的编译类路径上 …
    这有一些好处:

  • 不会增加任何传递性依赖

  • 减少类路径尺寸,加快编译速度
  • 当implementation 依赖发生改变减少重编译: 消费者不需要重新编译
  • 更干净的发布: 当与新的maven-publish 插件结合使用时,Java libraries 会产生POM文件(准确区分针对库编译所需的内容和在运行时使用库所需的内容) - 换句话说: 不混混淆编译库自身所需要的东西 以及 针对库编译所需要的东西 …

一个是面向项目本身,一个是面向此库的消费者编译 …

compile 以及 runtime 配置已经在Gradle 7.0已经被移除了,请参考upgrade guide 如何迁移到 implementation 以及 api 配置 …

如果你的构建消费了具有POM元数据的已发布模块,这个Java 以及 Java Library 插件同时信任api 和implementation 部分- 通过POM中使用的scope 进行分离 …
意味着compile 类路径仅仅包括Maven 的 compile 范围依赖 … 当然runtime 类路径增加Maven runtime 范围的 依赖 ..
对于已经发布到Maven的模块来说通常没有什么影响,这个项目定义的POM直接作为元数据发布 .. 这里compile 范围同时包括了 编译此项目所需要的依赖(例如implementation dependencies) 以及 针对已发布的库编译所需要的依赖(例如 API 依赖)

对于大多数已发布的库来说,这意味着所有的依赖属于compile scope. 如果你在存在的库上遇到了一些问题 ,你可以考虑使用component metadata rule 在构建中去修复错误的元数据 .. 然而正如上面所提到的,如果你使用Gradle 发布库,那么生成的POM文件仅仅将api 依赖放入compile scope中,其余的implementation 依赖放入 runtime scope;
如果你的构建消费了具有lvy 元数据的模块,你能够激活api 和implementation 分割(描述再此,如果所有的模块遵循某些同一个接口)

gradle 5.0 默认激活compile 和 runtime scope of modules 分割 ,gradle 4.6+,你需要通过在settings.gradle文件中增加 enableFeaturePreview(‘IMPROVED_POM_SUPPORT’)激活它 ..

Recognizing API and implementation dependencies

这个部分帮助你识别API 和 Implementation 依赖 - 通过经验法则

  • 偏好implementation 配置而不是api

这保持消费者的编译类路径保持稳定,如果任何一个implementation 类型意外的泄露为public API将导致消费者编译立即失败 ..
那么什么时候使用api 配置? 一个API 依赖通常包含一个或者多个由library 暴露的二进制 接口,这些通常指的是ABI(应用二进制接口),这些包括但是不限制:

  • 超类或者接口中使用的类型
  • 公共(public)方法参数使用的类型,包括泛型参数类型(这里的Public,是指对编译器可见,例如: 在Java世界中的public ,protected 以及 包私有的成员)
  • 公共字段使用的类型
  • 公共注解类型

作为对比,以下与ABI通常是无关的,它们应该声明为 implementation 依赖 :

  • 方法体中使用的特定类型
  • 私有成员中使用的特定类型
  • 内部类中发现的特定类型(Gradle 的特定版本 让你声明属于这种公共API的包)

例如,以下类使用一组三方库,其中一个暴露为类的Public API 并且其他内部使用 ..
import 语句并不能帮助我们判断谁是谁,我们需要自己查看字段、构造器、以及方法 ..

抽象

其实这里的说法就是抽象,与抽象无关的可以是implementation,对于生产者而言,例如它提出一个抽象,它可能只需要暴露规范,一些内部代码,对于消费者来说,根本没有任何帮助,所以它可以是iplementation ,而暴露的规范应该是API …
正如库 - 模块有变种的概念,例如API 变种 - 它的工件(以及依赖获取完全用不上implementation 配置上的依赖) …
而在消费者运行时,消费者应该消费这个库的运行时变种 - 工件 … 否则可能无法运行 …

Example: Making the difference between API and implementation

  1. // The following types can appear anywhere in the code
  2. // but say nothing about API or implementation usage
  3. import org.apache.commons.lang3.exception.ExceptionUtils;
  4. import org.apache.http.HttpEntity;
  5. import org.apache.http.HttpResponse;
  6. import org.apache.http.HttpStatus;
  7. import org.apache.http.client.HttpClient;
  8. import org.apache.http.client.methods.HttpGet;
  9. import java.io.ByteArrayOutputStream;
  10. import java.io.IOException;
  11. import java.io.UnsupportedEncodingException;
  12. public class HttpClientWrapper {
  13. private final HttpClient client; // private member: implementation details
  14. // HttpClient is used as a parameter of a public method
  15. // so "leaks" into the public API of this component
  16. public HttpClientWrapper(HttpClient client) {
  17. this.client = client;
  18. }
  19. // public methods belongs to your API
  20. public byte[] doRawGet(String url) {
  21. HttpGet request = new HttpGet(url);
  22. try {
  23. HttpEntity entity = doGet(request);
  24. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  25. entity.writeTo(baos);
  26. return baos.toByteArray();
  27. } catch (Exception e) {
  28. ExceptionUtils.rethrow(e); // this dependency is internal only
  29. } finally {
  30. request.releaseConnection();
  31. }
  32. return null;
  33. }
  34. // HttpGet and HttpEntity are used in a private method, so they don't belong to the API
  35. private HttpEntity doGet(HttpGet get) throws Exception {
  36. HttpResponse response = client.execute(get);
  37. if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
  38. System.err.println("Method failed: " + response.getStatusLine());
  39. }
  40. return response.getEntity();
  41. }
  42. }

HttpClientWrapper的公共构造器使用HttpClient 作为一个参数,所以它暴露给消费者因此它属于一个public API,但是HttpGet 以及HttpEntity 使用在私有方法的签名中,因此它们不认为它HttpClient是一个API dependency ..
换句话说,ExceptionUtils 类型,来自commons-lang 库,但是它仅仅使用在方法内部(不是签名),因此它是一个implementation dependency …
因此我们推断httpclient是一个API依赖,然而 commons-lang 是一个implementation 依赖 ..
这个结论翻译为声明如下:

  1. dependencies {
  2. api 'org.apache.httpcomponents:httpclient:4.5.7'
  3. implementation 'org.apache.commons:commons-lang3:3.5'
  4. }

The Java Library plugin configurations

这下面的一张图描述了在Java Library 插件使用的时候如何配置 configurations
The Java Library Plugin - 图1

  • 绿色部分的配置表示用户应该用来声明依赖
  • 粉色的配置仅当组件编译时被使用或者针对库运行时使用的配置
  • 蓝色的配置表示组件内部使用的配置

下一张图描述了测试configurations 配置
The Java Library Plugin - 图2
每一个配置的角色在表中进行说明

Table1. Java Library plugin — configurations used to declare dependencies

Configuration name Role Consumable? Resolvable? Description
api Declaring API dependencies no no 声明依赖将传递性暴露给消费者,指定编译时/运行时
implementation Declaring implementation dependencies no no 此组件内部使用,不暴露给消费者(但是运行时仍然暴露给消费者)
compileOnly 声明仅编译依赖项 no no 在编译时必须要的依赖,但是运行时不需要 .. 这通常包括在运行时发现时被遮蔽的依赖项
compileOnlyApi 声明仅编译 API 依赖项 no no 在编译时必须声明的依赖,但是并不是运行时所需要的,这通常包括在运行时发现的被遮蔽的依赖项
runtimeOnly 声明运行时依赖项 no no 此声明的依赖项仅仅在运行时依赖,编译时不依赖 ..
testImplementation 测试依赖 no no 这些依赖被用来编译测试
testCompileOnly 声明仅编译依赖 no no 这是您声明依赖项的地方,这些依赖项仅在测试编译时需要,但不应泄漏到运行时。这通常包括在运行时发现时被遮蔽的依赖项。
testRuntimeOnly 声明测试运行时依赖 no no 此声明的依赖在测试运行时需要,编译时不需要 …

Table 2. Java Library plugin — configurations used by consumers

Configuration name Role Consumable? Resolvable? Description
apiElements 针对这个库所编译 yes no 这个配置包含了此库的编译类路径,当通过java 编译器编译它的时候被使用
runtimeElements 为了执行此库 yes no 这个配置包含了此库的运行时类路径

Table3. Java Library plugin — configurations used by the library itself

Configuration name Role Consumable? Resolvable? Description
compileClasspath 为了编译此库 no yes 这个配置包含这个库的编译类路径
runtimeClasspath 为了执行此库 no yes 这个配置包含了这个库的运行时类路径
testCompileClasspath 为了编译此库的测试 no yes 这个配置包含了此库的测试编译类路径
testRuntimeClasspath 为了执行此库的测试 no yes 这个配置包含了此库的测试运行时编译类路径

Building Modules for the Java Module System

自从Java 9开始,Java自身提供一种模块化系统 - 它允许严格在编译时和运行时进行封装 .. 你能够将一个Java 库调整为Java 模块 - 通过创建一个module-info.java 文件在main/java源目录中.

src |___ main |___ java |___ module-info.java

在这个module info文件中,你需要声明一个模块名,指定你想要暴露模块的包(那些包) 以及你需要的其他模块(那些模块)

  1. module org.gradle.sample {
  2. requires com.google.gson; // real module
  3. requires org.apache.commons.lang3; // automatic module
  4. // commons-cli-1.4.jar is not a module and cannot be required
  5. }

为了告诉Java 编译器 一个Jar是一个模块,对比传统的Java library,Gradle 需要将它放置在模块路径上. 它是类路径的替代产物,这是告诉编译器有关编译依赖的传统方式.
Gradle 将自动的放置你依赖的Jar在模块路径上,而不是类路径,如果以下三件事成立:

  • java.modularity.inferModulePath 没有关闭
  • 正在构建一个模块(对比传统的库) 我们能够通过增加module-info.java 文件表达 ..(另一个选择是为Jar 的manifest 增加一个Automatic-Module-Name属性 ..)
  • 当前模块依赖的Jar本身就是一个模块,那么Gradle 基于 module-info.class的出现— 进行决定 … module 描述符的编译版本(在Jar中)(或者,除此之外,设置Jar manifest的Automatic-Module-Name属性)

声明模块依赖

这是一种直接的方式和依赖建立关系(在构建文件中声明 并且模块的依赖声明在module-info.java 文件中), 理想情况下声明应该和以下表进行同步 ..

Table4. Mapping between Java module directives and Gradle configurations to declare dependencies

Java 模块指令和Gradle 配置声明依赖进行映射

Java Module Directive Gradle Configuration Purpose
requires implementation 声明 implementation 依赖
requires transitive api 声明 API 依赖
requires static compileOnly 声明仅编译依赖
requires static transitive compileOnlyApi 生民仅编译API 依赖

gradle 当前不会自动的检测(如果依赖声明式同步的),这也许在将来的版本进行增加 ..
对于声明模块的更多详情,查看document on the Java Module System.

Declaring package visibility and servies

Java模块系统支持额外的更细腻度的封装概念.. - Gradle 当前自身正在做的 ..
例如: 你可能明确需要声明一个包作为你API的一部分并且它们仅仅在你的模块内部可见 … 现在,请参考documentation on the Java Module System 去学习怎样在Java模块中使用这些特性 …

Declaring module versions

Java Module 同样有一个版本 - 能够编码为模块身份的一部分 - 在 module-info.class 文件中 .. 这个版本在模块运行中能够被检测..

  1. version = '1.2'
  2. tasks.named('compileJava') {
  3. // use the project's version or define one directly
  4. options.javaModuleVersion = provider { project.version }
  5. }

使用非模块的libraries

你可能回使用一些外部库,例如 来自Maven Central的OSS库,并使用在你的模块Java项目中 .. 某些库,在他们的更新版本中它们已经完全是一个具有模块描述符的模块 .
举个例子,com.google.code.gson:gson:2.8.6 具有一个模块名: com.google.gson.
其他,例如 org.apache.commons:commons-lang3:3.10,它没有提供完整的模块描述符,但是它至少包含一个Automatic-Module-Name条目在它的manifest 文件中定义了模块的名称为 (org.apache.commons.lang3,例如),某些模块,仅仅只有名称作为模块描述,叫做自动模块 - 它会暴露它自己的所有的包 并且能够读取在模块路径上的所有模块 ..
第三种情况是传统的库 它们完全没有提供模块信息 - 例如 commons-cli:commons-cli:1.4,Gradle 将这些库放置在类路径上而不是模块路径上,然后类路径它被处理为一个模块(叫做未命名模块)- 由Java命名 …

  1. dependencies {
  2. implementation 'com.google.code.gson:gson:2.8.6' // real module
  3. implementation 'org.apache.commons:commons-lang3:3.10' // automatic module
  4. implementation 'commons-cli:commons-cli:1.4' // plain library
  5. }
  1. module org.gradle.sample.lib {
  2. requires com.google.gson; // real module
  3. requires org.apache.commons.lang3; // automatic module
  4. // commons-cli-1.4.jar is not a module and cannot be required
  5. }

然而一个real 模块不能够直接依赖一个未命名模块(只能通过增加命令行标识),automatic 模块能够看见未命名模块,因此如果你不能够避免依靠一个库(没有模块信息),你能够包装这个库为一个automatic module作为你项目的一部分 ..
另一种处理非模块的方式是 充实现有的JAR(使用模块描述器 - 自己使用artifact transforms),这个连接所使用的样例包含了一个小的buildSrc插件 注册了一个这样的转换(你能够使用它并因此调整你的需要) ..
这是有趣的,如果你想要构建一个完整的模块化应用 并且想要java 运行时信任任何事情作为一个真实的module …

禁用Java Module 支持

在很少的情况下,你可能想要禁用内置的Java Module 支持并且使用其他方式定义模块路径,为了这样做,你能够禁用这样的功能(自动放置任何Jar到模块路径上)..
然后Gradle 将具有模块信息的Jar放置在类路径上,即使你在source set中拥有module-info.java,这对应于 Gradle 版本 <7.0 的行为。
为了使它工作,你需要在(对所有的任务或者单独的任务)设置Java扩展modularity.inferModulePath = false

  1. java {
  2. modularity.inferModulePath = false
  3. }
  4. tasks.named('compileJava') {
  5. modularity.inferModulePath = false
  6. }

构建一个automatic module

如果可以,你应该总是编写一个完整的module-info.java描述符到你的模块中 ..
目前为止,这里有一些情况你可能需要考虑 - 对于仅仅为automatic module 提供一个module名:

  • 你工作在一个不是模块的库上,但是你想让它在下个版本可用 ..增加Automatic-Module-Name 是一个好的开始(大多数OSS 库目前也是这样做的)
  • 一个automatic module能够作为一个适配器(在真实模块以及在类路径上的传统库之间)

为了转变一个普通的项目为一个automatic 模块,仅仅增加一个具有模块名称的 manifest entry即可 ..

  1. tasks.named('jar') {
  2. manifest {
  3. attributes('Automatic-Module-Name': 'org.gradle.sample')
  4. }
  5. }

你也能够定义一个automatic 作为多项目的一部分(否则定义真正的模块,例如其他库的适配器),这样在Gradle构建工作良好. 由于automatic模块项目目前不能够被IDEA/Eclipse 正确识别 … 你能够解决这个问题 - 通过手动的增加这个Jar(automatic 模块内置的Jar)作为项目的依赖 -> (IDE的UI中无法发现的依赖)

使用classes 而不是jar进行编译

java-library的一个特性是 项目能够消费这个库(仅仅需要进行编译的类目录,而不是完整Jar),这启用了轻量的中间项目依赖作为资源处理(processResources task) 以及归档构造(jar 任务)不再执行(当在开发期间中仅有Java code 编译执行时)

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

为消费者增加内存使用

一个间接的结果是,最新的检查将需要更多的内存,因为Gradle 将会快照单独的类文件而不是单个Jar,这也许导致增加了内存消耗(对于大项目来说),但是对于compileJava任务 up-to-date在大多数情况下来说都是有好处的(例如改变资源不改变上游项目的compileJava任务的输入)

大型多项目在 Windows 上的构建性能显着下降

快照独立类文件的另一个副作用是,仅仅影响windows 系统,那就是性能显著下降(当在类路径上处理大量的类文件时)
这仅仅对于大型的多项目是需要关注的(当有大量的类在路径上 - 通过许多api 或者(不建议 compile 依赖时).
为了减轻这个情况,你能够设置org.gradle.java.compile-classpath-packaging 系统属性为true 去改变Java Library插件的行为(使用jars 而不是类目录 -对于在编译类路径上的一切)..
注意,通过在编译时触发所有的jar 任务,因此这里有其他的性能影响 以及潜在的副作用,他仅仅推荐激活这个动作(仅仅在windows上遭受了签名描述的性能问题时才需要激活)

发布一个library

除了将一个库发布到组件仓库,你有时也许需要打包一个库 以及它的依赖 - 在分发可交付成果中。Java Library Distribution Plugin 能够帮你做到这样 …