繁体   English   中英

在构建 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文件并添加了必要的requires语句,但我现在无法访问src/main/resources目录中的src/main/resources文件。

(有关完整示例,请参阅此 GitHub 项目。)

当我使用./gradlew run./gradlew installDist + 生成的包装器脚本时,我能够读取资源文件,但是当我从 IDE 运行我的应用程序时,我不能。

我向 JetBrains 提交了一个问题,我了解到 IDEA 使用的是模块路径,而 Gradle 默认使用的是类路径。 通过将以下块添加到我的build.gradle ,我能够让 Gradle......无法读取任何资源文件。

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

我尝试export我感兴趣的资源目录export为“包”,并且在编译时遇到了构建失败:

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

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

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

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


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


解决方案:

根据Slaw 的回答,我在build.gradle添加了以下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()
  }
}

旁注:有趣的是,如果我继续添加使run任务针对 JAR 执行的 Slaw 代码,尝试读取运行任务中的资源目录InputStream现在会抛出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 模块示例,该示例还链接到各种其他相关文档。


Gradle 和 Java 9 模块支持

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

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

一些社区插件,如java9-modularity 插件,试图添加支持。 本指南将更新有关如何在开发时使用内置 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指令。 当资源需要被其他模块访问时,你只需要为包含资源的包添加这样的指令,因为模块中的资源受到封装

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

  • 如果资源名称以“ .class ”结尾,则它不会被封装。
  • 包名称源自资源名称。 如果包名称是模块中的,则只有当包至少对调用者的模块开放时,此方法的调用者才能找到资源。 如果资源不在模块的包中,则不封装资源。

解决方案

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

使用插件

最简单的选择是使用现成的 Gradle 插件,它可以为您处理一切。 Building Java 9 Modules指南给出了一个这样的插件的例子,我认为它是目前最全面的: 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 项目以在模块路径上运行。 不幸的是,您考虑得越多,情况就会变得越复杂:

  • 测试代码。 你必须决定你的测试代码是在它自己的模块中还是被修补到主代码的模块中(假设你没有在单元测试的类路径上保留所有东西)。

    • 单独的模块:可能更容易配置( compileTestJavatest配置与compileJavarun配置大致相同); 然而,这仅允许“黑盒测试”,因为模块系统不允许拆分包(即您只能测试公共 API)。
    • 修补模块:允许“白盒测试”,但更难配置。 由于您不会有任何requires测试依赖项的指令,因此您必须添加适当的--add-modules--add-reads参数。 然后你必须考虑到大多数测试框架都需要反射访问; 由于您不太可能将主模块作为开放模块,因此您还必须添加适当的--add-opens参数。
  • 包装。 一个模块可以有一个主类,所以你只需要使用--module <module-name>而不是--module <module-name>/<mainclass-name> 这是通过使用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 在这方面走得更远。

在@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");
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM