在构建 Java 9 模块的 Gradle 项目中,资源文件位于何处?Gradle & Java 9 Module Support问题溶液

从IDEA 2018.2.1开始,IDE开始从已模块化的依赖项中突出显示“不在模块图中”的错误包。我向项目添加了一个文件并添加了必需的语句,但现在访问目录中的资源文件时遇到问题。module-info.javarequiressrc/main/resources

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

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

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

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

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

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

使用而不是得到相同的错误,尽管降级为警告。opensexports

我甚至尝试将资源目录移动到 下,但这会产生相同的错误/警告,并且还导致资源未复制到该目录。mydirsrc/main/javabuild

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


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


溶液:

根据Slaw的答案,我添加了以下内容: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 的代码来使任务针对 JAR 执行,则尝试读取运行任务中的资源目录现在会抛出 IOException,而不是提供文件列表。(针对 JAR,它只是得到一个空的 。)runInputStreamInputStream


答案 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 模块示例,该示例还链接到其他各种相关文档。


Gradle & Java 9 Module Support

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

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

一些社区插件,如java9模块化插件,试图添加支持。本指南将更新,以提供有关如何在开发时使用内置 Gradle 支持的更多信息。

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

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


问题

找不到资源文件的原因是,默认情况下,Gradle 会在不同的目录中输出已编译的类和已处理的资源。它看起来像这样:

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

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

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

注意:通过“仅资源包”,我的意思是包含资源的包,但没有一个资源具有.java.class扩展。

当然,如果资源只能由模块本身访问,那么添加指令就没有必要了。当其他模块需要访问资源时,只需为包含资源的包添加此类指令,因为模块中的资源需要封装opens

命名模块中的资源可以封装,以便无法通过其他模块中的代码找到它。是否可以找到资源,具体如下:

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

溶液

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

使用插件

最简单的选择是使用现成的Gradle插件,它可以为您处理所有内容。构建Java 9模块指南给出了一个这样的插件的例子,我认为它是目前最全面的:gradle-modules-plugin

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

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

手动指定适当的 JVM 选项

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

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

上述用途(参见java工具文档):--patch-module

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

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

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

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

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

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

整合类和资源

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

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

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

我相信,如果您期望使用纯资源包,则可能需要这样做。即使由于该指令,您也会在运行时收到错误,因为模块系统似乎在应用 之前执行了一些完整性验证。换句话说,纯资源包不会“足够快”地存在。我不确定是否有任何插件处理此用例。opens--patch-moduleopens--patch-module

但是,在编译时,允许包不存在,尽管会发出警告。话虽如此,可以通过在任务中使用来摆脱警告。opensjavac--patch-modulecompileJava

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

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


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


答案 2

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

opens io.fouad.packageone to moduletwo.name;

否则,将返回以下内容:null

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

考虑到该类在模块中,并且文件在模块中。Amoduletwo.namelogging.propertiesmoduleone.name


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

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

推荐