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
andtest
as forcompileJava
andrun
); 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.
- Separate module: Probably easier to configure (roughly the same configuration for
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 thejar
tool. Unfortunately, the GradleJar
task class does not have a way to specify this, as far as I can tell. One option is to usedoLast
andexec
to manually call thejar
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.
Beside @Slaw's answer (thanks to him), I had to open the package that contains the resources to the caller's module. As follows (moduleone.name module-info.java
):
opens io.fouad.packageone to moduletwo.name;
Otherwise, the following would return null
:
A.class.getResource("/io/fouad/packageone/logging.properties");
considering that class A
is in module moduletwo.name
and the file logging.properties
is inside module moduleone.name
.
Alternatively, moduleone.name
could expose a utility method that returns the resource:
public static URL getLoggingConfigFileAsResource()
{
return A.class.getResource("/io/fouad/packageone/logging.properties");
}