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

Update (25 March 2020): There has been significant progress towards proper JPMS support. A nightly build of Gradle 6.4 now includes options to develop with Java 9 modules natively. See https://github.com/gradle/gradle/issues/890#issuecomment-603289940 .

Update (29 September 2020): Since Gradle 6.4 (current release as of this update is 6.6.1) you can now support JPMS modules natively in Gradle projects, but you have to explicitly activate this feature:

java {
    modularity.inferModulePath.set(true)
}

See Gradle’s Java Modules Sample, which also links to various other pertinent documentation, for more information.


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: JPMS 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 (assuming you don’t keep everything on the classpath for unit testing).

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

Leave a Comment