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
.)
gradle file is located inside your project folder under app/build. gradle.
gradle files are located. The build. gradle (Project: MyApplication) file is in the root folder of the project and its configuration settings apply to every module in the project.
The Jar is created under the $project/build/libs/ folder.
In Android Studio, Right-clicking on the folder to add as library. Editing the dependencies in the build. gradle file, adding: compile fileTree(dir: 'libs', include: '*. jar')}
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.
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 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.
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.
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.
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).
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).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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With