【问题标题】:Where do resource files go in a Gradle project that builds a Java 9 module?在构建 Java 9 模块的 Gradle 项目中,资源文件放在哪里?
【发布时间】:2019-01-22 16:07:00
【问题描述】:

从 IDEA 2018.2.1 开始,IDE 启动错误突出显示包“not 在模块图中”来自已模块化的依赖项。我在我的项目中添加了一个module-info.java 文件并添加了 必要的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 我感兴趣的资源目录作为 “包”,并在编译时构建失败:

错误:包为空或不存在:mydir

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

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

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


注意:在继续阅读后,我对这个问题进行了大量编辑 研究问题。在最初的问题中,我也试图 弄清楚如何列出资源目录中的文件,但在 在调查过程中,我确定这是一条红鲱鱼—— 首先,因为读取资源目录仅在 正在从file:/// URL 读取资源(甚至可能不是这样), 其次,因为普通文件也不起作用,所以很明显 问题一般出在资源文件上,而不是专门与 目录。


解决方法:

根据Slaw's answer,我将以下内容添加到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。)

【问题讨论】:

  • 如果您正在使用命名模块,您是否也尝试过--add-opens <source-module>/<package>=<target-module>,其中包名称是根据Module#getResourceAsStream 的文档中所述派生的?顺便问一下,--show-module-resolution 在启动时列出了什么?
  • @nullpointer 这只是获取package fonts not in <module-name> 的另一种方式,尽管至少应用程序不会崩溃。同样,这是访问在同一模块中的资源目录的代码。如果这需要特殊的命令行处理,那么模块系统就会损坏。
  • @nullpointer --show-module-resolution 似乎没有显示任何非常有趣的东西。它确实给出了root <module-name> file:///<project-dir>/out/production/classes,是的,不包括out/production/resources
  • 有趣。我不知道您能够读取带有ClassLoader.getResourceAsStream() 的文件夹。直到。不确定这是否曾经是受支持的功能。
  • @DavidMoles 好的,如果您可以分享一个 MCVE 来理解和重现您所指出的内容,那就太好了。似乎您只有一个命名(您的)模块,并且您正在尝试访问其中的资源。但不是很清楚你想用代码ClassLoader.getSystemClassLoader().getResourceAsStream("fonts") 实现什么...只是补充一下,到 github 的链接对我也很有效。

标签: java gradle embedded-resource java-9 java-module


【解决方案1】:

更新(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 模块支持

不幸的是,从 6.0.1 版开始,Gradle 仍然没有对 Java 9 模块的一流支持,这可以从 Building Java 9 Modules 指南中看到。

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

一些community plugins,如java9-modularity plugin,尝试添加支持。本指南将在开发时提供有关如何使用内置 Gradle 支持的更多信息。

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

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


问题

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

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

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

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

注意:“仅资源包”是指包含资源但没有资源具有 .java.class 扩展名的包。

当然,如果资源只能由模块本身访问,则不需要添加opens 指令。当需要其他模块访问资源时,您只需为包含资源的包添加此类指令,因为模块中的资源受encapsulation的约束。

命名模块中的资源可能会被封装,因此其他模块中的代码无法定位它。是否可以定位到资源的判断方法如下:

  • 如果资源名称以“.class”结尾,则不封装。
  • 包名是从资源名派生的。如果包名是模块中的package,那么只有当包为open 时,该方法的调用者才能定位到至少调用者所在模块的资源。如果资源不在模块中的包中,则不封装资源。

解决方案

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

使用插件

最简单的选择是使用现成的 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):

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

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

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

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

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

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

整合类和资源

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

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

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

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

然而,在编译时,允许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()
    }
}

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


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

【讨论】:

  • 谢谢,这大大清除了它; Gradle 还没有一流的模块支持的提醒很好。
  • 这在今天是否仍然有效(Gradle 仍然没有对 Jigsaw 模块的一流支持)?
  • @elect 据我所知,Gradle 仍然没有对 Jigsaw 模块的一流支持。我在答案顶部附近添加了一个小更新。
  • @elect 相同的评论
【解决方案2】:

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

opens io.fouad.packageone to moduletwo.name;

否则,以下将返回null

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

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


或者,moduleone.name 可以公开一个返回资源的实用方法:

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

【讨论】:

    猜你喜欢
    • 2019-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-05
    • 2010-11-01
    相关资源
    最近更新 更多