Gradle jvm插件系列教程之Java Library插件权威详解
作者:BigDataMLApplication
【Gradle jvm插件系列4】 Java Library插件用法示例权威详解
使用方法
要使用Java Library插件,请在构建脚本中包含以下内容:
plugins { id 'java-library' }
API和实现分离
标准Java插件和Java Library插件之间的关键区别在于后者引入了向消费者公开的API概念。Java库是供其他组件消费的Java组件。在多项目构建中这是非常常见的用例,也适用于外部依赖。
该插件公开了两个配置,用于声明依赖项:api和implementation。api配置应该用于声明由库API导出的依赖项,而implementation配置应该用于声明组件内部的依赖项。
示例2:声明API和实现依赖项
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配置,请参考升级指南以了解如何迁移到implementation和api配置。
如果构建使用具有POM元数据的发布模块,则Java和Java Library插件都通过POM中使用的作用域来支持API和实现分离。这意味着编译类路径仅包括Maven compile范围的依赖项,而运行时类路径还包括Maven runtime范围的依赖项。
这通常对于使用Maven发布的模块没有影响,因为定义项目的POM文件直接作为元数据发布。在这种情况下,编译范围包括既用于编译项目的依赖项(即实现依赖项),也用于针对已发布库进行编译的依赖项(即API依赖项)。对于大多数已发布的库来说,这意味着所有依赖项都属于编译范围。如果您在现有库中遇到此类问题,请考虑使用组件元数据规则来修复构建中的错误元数据。但是,如上所述,如果使用Gradle发布库,则生成的POM文件将api依赖项放入compile范围,将其余的implementation依赖项放入runtime范围。
如果构建使用具有Ivy元数据的模块,并且所有模块都遵循特定结构,则可以按照此处描述的方式激活api和implementation分离。
从Gradle 5.0版本开始,默认情况下启用了模块的编译和运行时范围分离。从Gradle 4.6版本开始,您需要通过在settings.gradle中添加enableFeaturePreview('IMPROVED_POM_SUPPORT')
来激活它。
识别API和实现依赖项
本节将帮助您使用一些简单的经验法则来识别代码中的API和实现依赖项。首先的法则是:
在可能的情况下,优先使用implementation配置。
这样可以将依赖项保持在消费者的编译类路径之外。此外,如果任何实现类型意外泄漏到公共API中,消费者将立即无法编译。
那么什么时候应该使用api配置呢?API依赖项是至少包含一个在库二进制接口(ABI)中公开的类型的依赖项。这包括但不限于以下内容:
- 用于超类或接口的类型
- 用于公共方法参数的类型,包括泛型参数类型(其中“公共”是对编译器可见的内容。即Java世界中的public、protected和package-private成员)
- 用于公共字段的类型
- 公共注解类型
相反,在以下列表中使用的任何类型都与ABI无关,因此应将其声明为实现依赖项:
- 仅在方法体中使用的类型
- 仅在私有成员中使用的类型
- 仅在内部类中找到的类型(Gradle的未来版本将允许您声明哪些包属于公共API)
下面的示例代码使用了一些第三方库,其中一个库在类的公共API中暴露,另一个库只在内部使用。import语句无法帮助我们确定哪个是API依赖项,因此我们必须查看字段、构造函数和方法:
// 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。请注意,HttpGet和HttpEntity在私有方法的签名中使用,因此它们不计入使HttpClient成为API依赖项。
另一方面,来自commons-lang库的ExceptionUtils类型仅在方法体中使用(而不是在其签名中),因此它是实现依赖项。
因此,我们可以推断httpclient是一个API依赖项,而commons-lang是一个实现依赖项。这个结论可以转化为构建脚本中的以下声明:
dependencies { api 'org.apache.httpcomponents:httpclient:4.5.7' implementation 'org.apache.commons:commons-lang3:3.5' }
Java Library插件配置
下面的图表描述了在使用Java Library插件时如何设置配置。
绿色配置是用户应该用于声明依赖关系的配置。
粉色配置是组件在编译或与库运行时使用的配置。
蓝色配置是组件内部使用的配置,仅供其自身使用。
下一个图表描述了测试配置的设置:
下表描述了每个配置的作用:
表格1. Java Library插件 - 用于声明依赖关系的配置
配置名称 | 作用 | 可消耗性 | 可解析性 | 描述 |
---|---|---|---|---|
api | 声明API依赖关系 | 否 | 否 | 在这里声明传递导出到消费者的依赖关系,用于编译时和运行时。 |
implementation | 声明实现依赖关系 | 否 | 否 | 在这里声明纯粹为内部使用而不打算向消费者公开的依赖关系(在运行时仍然对消费者公开)。 |
compileOnly | 声明仅编译依赖关系 | 否 | 否 | 在这里声明在编译时需要但在运行时不需要的依赖关系。这通常包括在运行时找到时会被屏蔽的依赖关系。 |
compileOnlyApi | 声明仅编译API依赖关系 | 否 | 否 | 在这里声明模块和消费者在编译时需要但在运行时不需要的依赖关系。这通常包括在运行时找到时会被屏蔽的依赖关系。 |
runtimeOnly | 声明运行时依赖关系 | 否 | 否 | 在这里声明仅在运行时需要而不在编译时需要的依赖关系。 |
testImplementation | 测试依赖关系 | 否 | 否 | 在这里声明用于编译测试的依赖关系。 |
testCompileOnly | 声明仅测试编译依赖关系 | 否 | 否 | 在这里声明仅在测试编译时需要但不应泄露到运行时的依赖关系。这通常包括在运行时找到时会被屏蔽的依赖关系。 |
testRuntimeOnly | 声明测试运行时依赖关系 | 否 | 否 | 在这里声明仅在测试运行时需要而不在测试编译时需要的依赖关系。 |
表格2. Java Library插件 - 消费者使用的配置
配置名称 | 作用 | 可消耗性 | 可解析性 | 描述 |
---|---|---|---|---|
apiElements | 用于编译此库 | 是 | 否 | 此配置用于供消费者检索编译此库所需的所有元素。 |
runtimeElements | 用于执行此库 | 是 | 否 | 此配置用于供消费者检索运行此库所需的所有元素。 |
表格3. Java Library插件 - 库本身使用的配置
配置名称 | 作用 | 可消耗性 | 可解析性 | 描述 |
---|---|---|---|---|
compileClasspath | 用于编译此库 | 否 | 是 | 此配置包含此库的编译类路径,因此在调用Java编译器进行编译时使用。 |
runtimeClasspath | 用于执行此库 | 否 | 是 | 此配置包含此库的运行时类路径。 |
testCompileClasspath | 用于编译此库的测试 | 否 | 是 | 此配置包含此库的测试编译类路径。 |
testRuntimeClasspath | 用于执行此库的测试 | 否 | 是 | 此配置包含此库的测试运行时类路径。 |
为Java模块系统构建模块
自Java 9以来,Java本身提供了一个模块系统,允许在编译和运行时进行严格的封装。您可以通过在main/java源文件夹中创建一个module-info.java文件将Java库转换为Java模块。
src └── main └── java └── module-info.java
在module info文件中,您声明一个模块名称,您希望导出哪些模块包,并且您需要哪些其他模块。
// module-info.java file 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需要将其放置在所谓的模块路径上。这是与classpath相反的一种选择,classpath是告诉编译器关于已编译依赖关系的传统方式。如果以下三个条件为真,Gradle将自动将您的依赖项的Jar放置在模块路径上,而不是在classpath上:
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文件中声明的模块依赖关系之间存在直接关系。理想情况下,这些声明应该保持同步,如下表所示:
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 version = '1.2' tasks.named('compileJava') { // 使用项目的版本或直接定义一个版本 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 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自己向现有的Jars添加模块描述符。该示例包含一个小的buildSrc插件,用于注册这样的转换器,您可以使用并根据需要进行调整。如果您想构建一个完全模块化的应用程序,并希望Java运行时将所有内容视为真正的模块,则可能会对此感兴趣。
禁用Java模块支持
在极少数情况下,您可能希望禁用内置的Java模块支持,并通过其他方式定义模块路径。为了实现这一点,您可以禁用自动将任何Jar放置在模块路径上的功能。然后,即使在源集中具有module-info.java,Gradle也会将带有模块信息的Jars放置在类路径上。这对应于Gradle版本<7.0的行为。
要使其工作,您需要在Java扩展上(对于所有任务)或个别任务上设置 modularity.inferModulePath = false
。
// 示例:禁用Gradle的模块路径推断 build.gradle java { modularity.inferModulePath = false } tasks.named('compileJava') { modularity.inferModulePath = false }
构建自动模块
如果可以的话,您应该始终为您的模块编写完整的module-info.java描述符。但是,有一些情况下,您可能考虑(最初)只为自动模块提供模块名称:
- 您正在开发一个不是模块的库,但是您希望在下一个版本中将其用作模块。添加Automatic-Module-Name是一个很好的第一步(Maven中央的大多数热门OSS库现在已经这样做了)。
- 如前一节所讨论的,自动模块可以用作真正模块和类路径上的传统库之间的适配器。
要将普通Java项目转换为自动模块,只需添加具有模块名称的清单条目:
// 示例:在Jar清单属性中声明自动模块名称 build.gradle tasks.named('jar') { manifest { attributes('Automatic-Module-Name': 'org.gradle.sample') } }
您可以将自动模块定义为多项目的一部分,该项目还定义了真正的模块(例如,作为与另一个库的适配器)。尽管Gradle构建中的这种方式运行良好,但IDEA / Eclipse当前无法正确识别此类自动模块项目。您可以通过在IDE的UI中手动将为自动模块构建的Jar添加到无法找到它的项目的依赖项中来解决此问题。
使用类而不是jar进行编译
java-library插件的一个特性是,消费该库的项目在编译时只需要classes文件夹,而不需要完整的JAR文件。这样可以实现更轻量级的项目间依赖,因为只有在开发过程中执行Java代码编译时才会执行资源处理(processResources任务)和归档构建(jar任务)。
使用classes输出而不是JAR是由消费者决定的。例如,Groovy消费者可能会请求classes和已处理的资源,因为这些可能在编译过程中执行AST转换所需。
消费者的内存使用增加
一个间接的后果是,增量检查将需要更多的内存,因为Gradle将对单个类文件进行快照,而不是单个jar文件。这可能会导致大型项目的内存消耗增加,在某些情况下(例如,更改资源不再更改上游项目的compileJava任务的输入),使得compileJava任务更容易处于最新状态。
对于庞大的多项目,Windows系统会出现显著的构建性能下降
对于快照处理的单个类文件,仅影响Windows系统,当处理大量类文件时,性能可能显著下降。这仅涉及非常大的多项目,其中通过使用许多api或(已弃用的)compile依赖项在类路径上存在许多类。为了缓解这个问题,您可以将org.gradle.java.compile-classpath-packaging
系统属性设置为true,以改变Java Library插件的行为,使用jar而不是class文件夹来处理编译类路径上的所有内容。请注意,由于这会产生其他性能影响和潜在的副作用(通过触发所有jar任务进行编译),只建议在Windows上遇到上述性能问题时激活此选项。
发布库
除了将库发布到组件存储库外,有时您可能需要将库及其依赖项打包到分发包中。Java Library Distribution插件就是为了帮助您完成这个任务。
参考链接
参考链接
小军李:【Gradle jvm插件系列1】 Java Application插件权威详解
小军李:【Gradle jvm插件系列2】 Java Library插件用法示例权威详解
小军李:【Gradle jvm插件系列3】 Java platform平台插件权威详解
小军李:【Gradle jvm插件系列4】 scala插件权威详解
小军李:【gradle多模块系列1】多项目构建和子项目的添加管理
小军李:【Gradle多模块系列2】在子项目之间声明依赖关系和共享构建逻辑示例详解
小军李:【Gradle 多模块系列3】如何开发自定义Gradle插件
到此这篇关于Java Library插件权威详解的文章就介绍到这了,更多相关Java Library插件内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!