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

David Moles :

As of IDEA 2018.2.1, the IDE starts error-highlighting packages "not in the module graph" from dependencies that have been modularized. I added a module-info.java file to my project and added the requisite requires statements, but I'm now having trouble accessing resource files in my src/main/resources directory.

(For a complete example, see this GitHub project.)

When I used ./gradlew run or ./gradlew installDist + the resulting wrapper script, I was able to read resource files, but when I ran my app from the IDE, I wasn't.

I filed an issue with JetBrains, and what I learned was that IDEA was using the module path, while Gradle, by default, was using the classpath. By adding the following block to my build.gradle, I was able to get Gradle to also … not be able to read any resource files.

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

I tried export-ing the resource directory I was interested in as a "package", and at compile time got a build failure with:

error: package is empty or does not exist: mydir

Using opens instead of exports got the same error, though downgraded to a warning.

I even tried moving the mydir resource directory under src/main/java, but this produced the same errors / warnings, and also resulted in the resources not being copied to the build directory.

Where are resources supposed to go in Java 9, and how do I access them?


Note: I've edited this question considerably after continuing to research the problem. In the initial question, I was also trying to figure out how to list the files in a resource directory, but in the course of the investigation I determined that was a red herring -- first, because reading resource directories only works when the resource is being read from a file:/// URL (and maybe not even then), and second because plain files weren't working either, so clearly the problem was with resource files in general and not specifically with directories.


Solution:

Per Slaw's answer, I added the following to 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()
  }
}

Side note: Interestingly, if I don't continue on to add Slaw's code that makes the run task execute against the JAR, attempting to read a resource directory InputStream in the run task now throws an IOException instead of providing the list of files. (Against the JAR, it simply gets an empty InputStream.)

Slaw :

Gradle & Java 9 Module Support

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.

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.

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.

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


The Problem

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/

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

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.

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

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:

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

Use a 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 "..."
}

You can also check out other available plugins.

Manually Specify Appropriate JVM Options

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

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

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

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.

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

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

Consolidate Classes and Resources

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

sourceSets {
    main {
        output.resourcesDir = java.outputDir
    }
}

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.

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.

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

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.


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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=35844&siteId=1