更新(2020年3月25日): 在获得JPMS适当支持方面已经取得了重大进展。每晚构建的Gradle 6.4现在包含使用Java
9模块本地开发的选项。见https://github.com/gradle/gradle/issues/890#issuecomment-603289940。
Gradle和Java 9模块支持
不幸的是,从6.0.1版开始,Gradle仍然没有对Java 9模块的一流支持,这可以从《构建Java
9模块》指南中看到。
Java 9的最令人兴奋的功能之一是它对开发和部署模块化Java软件的支持。Gradle还没有对Java 9模块的一流支持。
一些社区插件,例如java9-modularity插件,试图增加支持。本指南将在开发时更新有关如何使用内置Gradle支持的更多信息。
注意: 本指南过去更加详尽,并提供了有关如何“手动”自定义现有任务的示例。但是,此后更改为上面的建议,建议使用至少提供某些Java
9支持的第三方插件。这些社区插件中的某些似乎不仅仅提供模块支持,例如对使用
jlinkGradle工具的支持。
Gradle项目有一个“史诗级”,据称可以跟踪Java
9模块支持:JPMS支持#890。
问题
找不到资源文件的原因是,默认情况下,Gradle将已编译的类和已处理的资源输出到不同的目录中。看起来像这样:
build/|--classes/|--resources/
该
classes目录是
module-info.class文件放置。这会给模块系统带来问题,因为从技术上讲,目录下的
resources文件不包含在目录中的模块内
classes。当使用类路径而不是模块路径时,这不是问题,因为模块系统将整个类路径视为一个巨型模块(即所谓的
未命名模块 )。
如果
opens为仅资源包添加指令,则在运行时会出现错误。错误的原因是由于上述目录布局,软件包在模块中不存在。出于相同的原因,在编译时会收到警告。该模块存在,
src/main/java并且该模块下的资源文件在
src/main/resources技术上不包括在该模块中。
注意: “仅资源包”是指包含资源但没有资源具有.java
或.class
扩展名的包。
当然,如果仅模块本身可以访问资源,则
opens无需添加指令。仅当需要使其他模块可以访问资源时,才需要为包含资源的软件包添加此类指令,因为模块中的资源需要封装。
可以 封装 命名模块中的资源,以使其他模块中的代码无法定位该资源。确定是否可以定位资源如下:
- 如果资源名称以“
.class” 结尾,则不会被封装。- 一 包名
是从资源名称的。如果程序包名称是模块中的程序包,则仅当程序包至少对调用方的模块打开时,该方法的调用方才能找到资源。如果资源不在模块的包装中,则不会封装该资源。
解
最终,解决方案是确保将资源视为模块的一部分。但是,有几种方法可以做到这一点。
使用插件
最简单的选择是使用现成的Gradle插件,该插件可以为您处理所有事情。《 构建Java 9模块》
指南提供了一个这样的插件的示例,我相信它是目前最全面的插件:gradle-modules-plugin。
plugins { id("org.javamodularity.moduleplugin") version "..."}您还可以查看其他可用的插件。
手动指定适当的JVM选项
另一个选项是配置每个需要的Gradle任务以指定一些JVM选项。由于您主要关注从模块内部访问资源,因此需要配置
run任务以使用资源目录修补模块。这是一个示例(Kotlin
DSL):
plugins { application}group = "..."version = "..."java { sourceCompatibility = JavaVersion.VERSION_13}application { mainClassName = "<module-name>/<mainclass-name>"}tasks { compileJava { doFirst { options.compilerArgs = listOf( "--module-path", classpath.asPath, "--module-version", "${project.version}" ) classpath = files() } } named<JavaExec>("run") { doFirst { val main by sourceSets jvmArgs = listOf( "--module-path", classpath.asPath, "--patch-module", "<module-name>=${main.output.resourcesDir}", "--module", application.mainClassName ) classpath = files() } }}上面的用法
--patch-module(请参阅
java工具文档):
使用JAR文件或目录中的类和资源覆盖或扩展模块。
如果使用上面的示例,它将获得一个简单的Gradle项目,以在模块路径上运行。不幸的是,您考虑的越多,情况就越复杂:
测试代码。您必须确定测试代码是放在自己的模块中还是被修补到主代码的模块中(假设您没有将所有内容都保留在类路径中以进行单元测试)。
- 单独的模块:可能更容易配置(大致为相同的结构
compileTestJava
和test
作为用于compileJava
和run
); 但是,由于模块系统不允许拆分包,因此,这仅允许“黑盒测试”(即,您只能测试公共API)。 - 修补模块:允许进行“白盒测试”,但较难配置。由于您没有
requires
用于测试依赖项的任何指令,因此必须添加适当的--add-modules
和--add-reads
参数。然后,您必须考虑到大多数测试框架都需要反射访问。由于您不太可能将主模块作为开放模块,因此还必须添加适当的--add-opens
参数。 - 打包。一个模块可以有一个主类,所以你只需要使用
--module <module-name>
代替--module <module-name>/<mainclass-name>
。这是通过--main-class
使用jar
工具指定选项来完成的。不幸的是,Jar
据我所知,Gradle 任务类没有指定此方法。一种选择是使用doLast
并exec
手动调用该jar
工具和--update
JAR文件。
- 单独的模块:可能更容易配置(大致为相同的结构
该
application
插件还添加了创建启动脚本的任务(例如批处理文件)。假设您需要这些脚本,则必须将其配置为使用模块路径而不是类路径。
基本上,我强烈建议您使用插件。
整合课程和资源
第三种选择是将已处理资源配置为具有与已编译类相同的输出目录。
sourceSets { main { output.setResourcesDir(java.outputDir) }}注意: 在将资源输出设置为与Java输出相同的情况下,可能有必要使用配置jar
任务duplicatesStrategy =DuplicatesStrategy.EXCLUDE
。
我相信,如果您希望使用
opens仅资源包,则可能需要这样做。即使
--patch-module由于
opens指令而在运行时也会出错,因为模块系统似乎在应用之前执行了一些完整性验证
--patch-module。换句话说,仅资源包不会很快“存在”。我不确定是否有任何插件可以处理此用例。
但是,在编译时,允许一个
opens软件包不存在,尽管它
javac会发出警告。话虽如此,有可能通过
--patch-module在
compileJava任务中使用来消除警告。
tasks.compileJava { doFirst { val main by sourceSets options.compilerArgs = listOf( "--module-path", classpath.asPath, "--patch-module", "<module-name>=${main.resources.sourceDirectories.asPath}" "--module-version", "${project.version}" ) classpath = files() }}将资源和类合并到同一位置的另一种方法是将
run任务配置为针对由
jar任务构建的JAR文件执行。
希望Gradle很快会以一流的方式支持Java 9模块。我相信Maven在这方面要走得更远。



