且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

在构建 Java 9 模块的 Gradle 项目中,资源文件在哪里?

更新时间:2023-11-17 22:51:40

更新(2020 年 3 月 25 日):在正确支持 JPMS 方面取得了重大进展.Gradle 6.4 的每晚构建现在包括使用 Java 9 模块进行本地开发的选项.请参阅 https://github.com/gradle/gradle/issues/890#issuecomment-603289940 .

Update (25 March 2020): There has been significant progress towards proper JPMS support. A nightly build of Gradle 6.4 now includes options to develop with Java 9 modules natively. See https://github.com/gradle/gradle/issues/890#issuecomment-603289940 .

更新(2020 年 9 月 29 日):自 Gradle 6.4(本次更新的当前版本为 6.6.1)以来,您现在可以在 Gradle 项目中原生支持 JPMS 模块,但您必须明确激活此功能:

Update (29 September 2020): Since Gradle 6.4 (current release as of this update is 6.6.1) you can now support JPMS modules natively in Gradle projects, but you have to explicitly activate this feature:

java {
    modularity.inferModulePath.set(true)
}

请参阅 Gradle 的 Java 模块示例,其中还包括各种其他相关文档的链接,以获取更多信息.

See Gradle's Java Modules Sample, which also links to various other pertinent documentation, for more information.

不幸的是,从 6.0.1 版本开始,Gradle 仍然没有对 Java 9 模块的一流支持,从 构建 Java 9 模块 指南.

Unfortunately, Gradle still—as of version 6.0.1—does not have first class support for Java 9 modules, as can be seen by the Building Java 9 Modules guide.

Java 9 最令人兴奋的特性之一是它支持开发和部署模块化 Java 软件.Gradle 还没有对 Java 9 模块的一流支持.

One of the most exciting features of Java 9 is its support for developing and deploying modular Java software. Gradle doesn’t have first-class support for Java 9 modules yet.

一些社区插件,例如java9-modularity 插件,尝试添加支持.本指南将更新有关如何在开发时使用内置 Gradle 支持的更多信息.

Some community plugins, like java9-modularity plugin, attempt to add support. This guide will be updated with more information on how to use built-in Gradle support when it is developed.

注意:本指南过去更为广泛,并提供了有关如何手动"自定义现有任务的示例.但是,它已更改为上述建议使用至少提供一些 Java 9 支持的第三方插件.其中一些社区插件似乎不仅仅提供模块支持,例如支持使用 Gradle 的 jlink 工具.

Note: This guide used to be more extensive and offered examples on how to customize existing tasks "manually". However, it has since changed to the above which recommends using third-party plugins that offer at least some Java 9 support. Some of these community plugins appear to offer more than just module support, such as support for using the jlink tool from Gradle.

Gradle 项目有一个史诗"据称跟踪 Java 9 模块支持:JPMS Support #890.

The Gradle project has an "epic" which supposedly tracks Java 9 module support: JPMS Support #890.

你找不到你的资源文件的原因是因为Gradle默认输出不同目录下编译的类和处理过的资源.它看起来像这样:

The reason you can't find your resource files is because Gradle, by default, outputs the compiled classes and processed resources in different directories. It looks something like this:

build/
|--classes/
|--resources/

classes 目录是放置 module-info.class 文件的地方.这会导致模块系统出现问题,因为从技术上讲,resources 目录下的文件不包含在 classes 目录中的模块中.当使用类路径而不是模块路径时,这不是问题,因为模块系统将整个类路径视为一个巨大的模块(即所谓的未命名模块).

The classes directory is where the module-info.class file is placed. This causes a problem for the module system since technically the files under the resources directory are not included within the module present in the classes directory. This isn't a problem when using the classpath instead of the modulepath since the module system treats the whole classpath as one giant module (i.e. the so-called unnamed module).

如果您为纯资源包添加 opens 指令,您将在运行时收到错误.错误的原因是由于上述目录布局,模块中不存在该包.出于基本相同的原因,您在编译时收到警告;该模块存在于 src/main/java 中,并且 src/main/resources 下的资源文件在技术上不包含在该模块中.

If you add an opens directive for a resource-only package you'll get an error at runtime. The cause of the error being that the package does not exist in the module because of the aforementioned directory layout. You get a warning at compile-time for basically the same reason; the module is present in src/main/java and the resource files under src/main/resources are not technically included in that module.

注意:通过仅资源包"我的意思是包含资源但没有资源具有 .java.class 扩展名的包.

Note: By "resource-only package" I mean packages which contain resources but none of the resources have a .java or .class extension.

当然,如果资源仅供模块本身访问,则不需要添加 opens 指令.当其他模块需要访问资源时,您只需要为包含资源的包添加此类指令,因为模块中的资源受 封装.

Of course, if the resources are only to be accessible to the module itself then adding the opens directive should not be necessary. You only need to add such directives for resource-containing packages when resources need to be accessible to other modules because resources in modules are subject to encapsulation.

命名模块中的资源可能被封装,因此它不能被其他模块中的代码定位.一个资源是否可以被定位如下:

A resource in a named module may be encapsulated so that it cannot be located by code in other modules. Whether a resource can be located or not is determined as follows:

  • 如果资源名称以.class"结尾那么它就没有被封装.
  • 包名源自资源名称.如果包名称是 则该资源只能由该方法的调用者在包为 打开到至少调用者的模块.如果资源不在模块的包中,则不会封装资源.
  • If the resource name ends with ".class" then it is not encapsulated.
  • A package name is derived from the resource name. If the package name is a package in the module then the resource can only be located by the caller of this method when the package is open to at least the caller's module. If the resource is not in a package in the module then the resource is not encapsulated.


解决方案

最终的解决方案是确保资源被视为模块的一部分.但是,有几种方法可以做到这一点.


Solution

Ultimately the solution is to ensure the resources are considered part of the module. However, there are a few ways in which to do that.

最简单的选择是使用现成的 Gradle 插件,它可以为您处理一切.Building Java 9 Modules 指南给出了一个这样的插件的例子,我认为它是目前最全面的:gradle-modules-plugin.

The easiest option is to use a ready-made Gradle plugin which handles everything for you. The Building Java 9 Modules guide gives an example of one such plugin, which I believe is currently the most comprehensive: gradle-modules-plugin.

plugins {
    id("org.javamodularity.moduleplugin") version "..."
}

您还可以查看其他可用插件.

另一个选项是配置每个需要的 Gradle 任务来指定一些 JVM 选项.由于您主要关心从模块内部访问资源,因此您需要配置 run 任务以使用资源目录修补模块.这是一个示例(Kotlin DSL):

Another option is to configure each needed Gradle task to specify some JVM options. Since you're primarily concerned with accessing resources from within the module you need to configure the run task to patch the module with the resources directory. Here's an example (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 工具文档):

The above uses --patch-module (see java tool documentation):

使用 JAR 文件或目录中的类和资源覆盖或扩充模块.

Overrides or augments a module with classes and resources in JAR files or directories.

如果你使用上面的例子,它会得到一个简单的 Gradle 项目在模块路径上运行.不幸的是,您考虑得越多,情况就会变得越复杂:

If you use the example above it will get a simple Gradle project to run on the module path. Unfortunately, this gets much more complicated the more you consider:

  • 测试代码.您必须决定您的测试代码是在它自己的模块中还是被修补到主代码的模块中(假设您没有将所有内容都保留在类路径中以进行单元测试).

  • Test code. You have to decide if your test code will be in its own module or be patched into the main code's module (assuming you don't keep everything on the classpath for unit testing).

  • 单独的模块:可能更容易配置(compileTestJavatestcompileJavarun 的配置大致相同代码>);然而,这仅允许黑盒测试"由于模块系统不允许拆分包(即您只能测试公共 API).
  • 修补模块:允许白盒测试";但更难配置.由于您不会有任何用于测试依赖项的 requires 指令,因此您必须添加适当的 --add-modules--add-reads代码> 参数.然后你必须考虑到大多数测试框架都需要反射访问;由于您不太可能将主模块作为开放模块,因此您还必须添加适当的 --add-opens 参数.
  • Separate module: Probably easier to configure (roughly the same configuration for compileTestJava and test as for compileJava and run); however, this only allows for "blackbox testing" due to the fact split packages are not allowed by the module system (i.e. you can only test public API).
  • Patched module: Allows for "whitebox testing" but is harder to configure. Since you won't have any requires directives for test dependencies you'll have to add the appropriate --add-modules and --add-reads arguments. Then you have to take into account that most testing frameworks require reflective access; since you're unlikely to have your main module as an open module you'll have to add appropriate --add-opens arguments as well.

包装.一个模块可以有一个主类,所以你只需要使用 --module 而不是 --module /代码>.这是通过使用 jar 工具指定 --main-class 选项来完成的.不幸的是,据我所知,Gradle Jar 任务类没有办法指定这一点.一种选择是使用doLastexec 手动调用jar 工具和--update JAR 文件.

Packaging. A module can have a main class so you only have to use --module <module-name> instead of --module <module-name>/<mainclass-name>. This is done by specifying the --main-class option with the jar tool. Unfortunately, the Gradle Jar task class does not have a way to specify this, as far as I can tell. One option is to use doLast and exec to manually call the jar tool and --update the JAR file.

application 插件还添加了创建启动脚本的任务(例如批处理文件).假设您需要这些脚本,则必须将其配置为使用模块路径而不是类路径.

The application plugin also adds tasks to create start scripts (e.g. batch file). This will have to be configured to use the modulepath instead of the classpath, assuming you need these scripts.

基本上,我强烈建议使用插件.

Basically, I highly recommend using a plugin.

第三个选项是将处理后的资源配置为与编译后的类具有相同的输出目录.

A third option is to configure the processed resources to have the same output directory as the compiled classes.

sourceSets {
    main {
        output.setResourcesDir(java.outputDir)
    }
}

注意:在设置duplicatesStrategy = DuplicatesStrategy.EXCLUDE时可能需要配置jar任务资源输出与 Java 输出相同.

Note: It may be necessary to configure the jar task with duplicatesStrategy = DuplicatesStrategy.EXCLUDE when setting the resources output the same as the Java output.

如果您希望打开纯资源包,我相信这可能是必需的.即使使用 --patch-module 你也会因为 opens 指令在运行时得到错误,因为模块系统似乎在应用 之前执行了一些完整性验证 --补丁模块.换句话说,仅资源包将不会足够快"存在.我不确定是否有任何插件处理这个用例.

I believe this may be required if you expect to opens resource-only packages. Even with --patch-module you'll get an error at runtime due to the opens directive since the module system appears to perform some integrity validation before applying --patch-module. In other words, the resource-only package won't exist "soon enough". I'm not sure if any plugin handles this use case.

然而,在编译时,允许 opens 包不存在,尽管 javac 会发出警告.话虽如此,可以通过在 compileJava 任务中使用 --patch-module 来消除警告.

At compile-time, however, it's permissible for an opens package to not exist, though javac will emit a warning. That being said, it's possible to get rid of the warning by using --patch-module in the compileJava task.

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 文件执行.

Another way to consolidate the resources and classes into the same place is to configure the run task to execute against the JAR file built by the jar task.

希望 Gradle 很快就能以一流的方式支持 Java 9 模块.我相信 Maven 在这方面走得更远.

Hopefully Gradle will support Java 9 modules in a first-class manner some time soon. I believe Maven is further along in this respect.