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

Where do resource files go in a Gradle project that builds a Java 9 module?

从 IDEA 2018.2.1 开始,IDE 启动错误高亮包“不 在模块图中“来自已模块化的依赖项。我向我的项目添加了一个 module-info.java 文件并添加了 requisite requires 语句,但我现在无法访问 我的 src/main/resources 目录中的资源文件。

(有关完整示例,请参阅 this GitHub project。)

当我使用 ./gradlew run./gradlew installDist + 结果 包装脚本,我能够读取资源文件,但是当我 运行 我的 来自 IDE 的应用程序,我不是。

我提交了 issue 使用 JetBrains,我了解到 IDEA 正在使用该模块 路径,而 Gradle 默认情况下使用类路径。通过增加 我的build.gradle的以下块,我能够得到Gradle …无法读取任何资源文件。

run {
    inputs.property("moduleName", moduleName)
    doFirst {
        jvmArgs = [
                '--module-path', classpath.asPath,
                '--module', "$moduleName/$mainClassName"
        ]
        classpath = files()
    }
}

我尝试 export-ing 我感兴趣的资源目录 "package",并且在编译时出现构建失败:

error: package is empty or does not exist: mydir

使用 opens 而不是 exports 得到了同样的错误,虽然降级了 警告。

我什至尝试将 mydir 资源目录移动到 src/main/java 下, 但这产生了相同的错误/警告,并且还导致了 资源未复制到 build 目录。

Java9 中的资源应该放在哪里,我如何访问它们?


注意: 在继续 研究问题。在最初的问题中,我也试图 找出如何列出资源目录中的文件,但在 在调查过程中,我确定那是一条红鲱鱼—— 首先,因为读取资源目录只在 正在从 file:/// URL 中读取资源(甚至可能不会), 其次是因为普通文件也不起作用,所以很明显 问题一般是资源文件,而不是具体的 目录。


解法:

根据 ,我将以下内容添加到 build.gradle

// at compile time, put resources in same directories as classes
sourceSets {
  main.output.resourcesDir = main.java.outputDir
}

// at compile time, include resources in module
compileJava {
  inputs.property("moduleName", moduleName)
  doFirst {
    options.compilerArgs = [
      '--module-path', classpath.asPath,
      '--patch-module', "$moduleName=" 
        + files(sourceSets.main.resources.srcDirs).asPath,
      '--module-version', "$moduleVersion"
    ]
    classpath = files()
  }
}

// at run time, make Gradle use the module path
run {
  inputs.property("moduleName", moduleName)
  doFirst {
    jvmArgs = [
      '--module-path', classpath.asPath,
      '--module', "$moduleName/$mainClassName"
    ]
    classpath = files()
  }
}

旁注:有趣的是,如果我继续添加 Slaw 的代码来完成 run 任务针对 JAR 执行,尝试读取 运行 任务中的资源目录 InputStream 现在 throws an IOException 而不是提供文件列表。 (针对 JAR,它只是得到一个空的 InputStream。)

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

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

java {
    modularity.inferModulePath.set(true)
}

请参阅 Gradle 的 Java Modules Sample,其中还链接到各种其他相关文档,以获取更多信息。


Gradle & Java 9 模块支持

不幸的是,Gradle 仍然——从 6.0.1 版本开始——没有首先 class 支持 Java 9 个模块,正如 Building Java 9 Modules 指南.

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.

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 支持的 third-party 插件。其中一些社区插件似乎提供的不仅仅是模块支持,例如支持使用来自 Gradle 的 jlink 工具。

Gradle 项目有一个“史诗”,据说它跟踪 Java 9 模块支持:JPMS Support #890


问题

找不到资源文件的原因是Gradle默认将编译后的classes和处理后的资源输出到不同的目录。它看起来像这样:

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

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

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

注意: “resource-only 包”是指包含资源但 none资源有 .java.class 扩展名。

当然,如果资源仅供模块本身访问,则不需要添加 opens 指令。当资源需要被其他模块访问时,你只需要为 resource-containing 包添加这样的指令,因为模块中的资源受制于 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:

  • 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.

解决方案

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

使用插件

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

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

您还可以查看 other available plugins

手动指定适当的 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 tool documentation):

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

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

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

    • 单独的模块:可能更容易配置(compileTestJavatest 的配置与 compileJavarun 的配置大致相同);然而,这只允许“黑盒测试”,因为模块系统不允许拆分包(即你只能测试 public API)。
    • 修补模块:允许“白盒测试”但更难配置。由于您没有任何 requires 指令用于测试依赖项,因此您必须添加适当的 --add-modules--add-reads 参数。然后你必须考虑到大多数测试框架都需要反射访问;因为您不太可能将主模块作为开放模块,所以您将拥有还要添加适当的 --add-opens 参数。
  • 打包。一个模块可以有一个 main class,所以你只需要使用 --module <module-name> 而不是 --module <module-name>/<mainclass-name>。这是通过使用 jar 工具指定 --main-class 选项来完成的。不幸的是,据我所知,Gradle Jar 任务 class 没有办法指定它。一种选择是使用 doLastexec 手动调用 jar 工具和 --update JAR 文件。

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

基本上,我强烈推荐使用插件。

整合类和资源

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

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

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

如果您希望 opens resource-only 包,我相信这可能是必需的。由于 opens 指令,即使使用 --patch-module,您也会在 运行 时收到错误,因为模块系统似乎在应用 --patch-module 之前执行了一些完整性验证。换句话说,resource-only 包不会“很快”存在。我不确定是否有任何插件可以处理这个用例。

然而,在 compile-time,允许 opens 包不存在,尽管 javac 会发出警告。也就是说,可以通过在 compileJava 任务中使用 --patch-module 来消除警告。

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()
    }
}

另一种将资源和 classes 合并到同一个地方的方法是配置 run 任务以针对 jar 任务构建的 JAR 文件执行。


希望 Gradle 不久将以 first-class 的方式支持 Java 9 个模块。我相信 Maven 在这方面走得更远。

除了@Slaw 的回答(感谢他),我不得不打开包含调用者模块资源的包。如下(moduleone.name module-info.java):

opens io.fouad.packageone to moduletwo.name;

否则,下面会 return null:

A.class.getResource("/io/fouad/packageone/logging.properties");

考虑到 class A 在模块 moduletwo.name 中,文件 logging.properties 在模块 moduleone.name.


或者,moduleone.name 可以公开 return 资源的实用方法:

public static URL getLoggingConfigFileAsResource()
{
    return A.class.getResource("/io/fouad/packageone/logging.properties");
}