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

只谈情不闲聊 提交于 2019-11-27 14:23:56

From Gradle's epic regarding Jigsaw support I've learned of a plugin that may ease the process described below: gradle-modules-plugin. The epic also mentions other plugins such as chainsaw (which is a fork of the experimental-jigsaw plugin). Unfortunately, I haven't tried any of them as of yet so I can't comment on how well they handle resources, if at all. I recommend trying gradle-modules-plugin, it seems to handle at least the basic configurations needed to use Jigsaw modules rather painlessly. That said, as far as I can tell, there's still no first-class support for Jigsaw modules as of Gradle 5.5.1.


In your bounty you request official documentation about "the right way" to handle resources with Gradle and Jigsaw modules. The answer, as far as I know, is that there is no "right way" because Gradle still (as of 4.10-rc-2) doesn't have first-class support for Jigsaw modules. The closest you get is the Building Java 9 Modules document.

However, you mention this is about accessing resources from within a module (i.e. not from external modules). This shouldn't be too hard to fix with a simple build.gradle configuration.

By default, Gradle separates the output directories for classes and resources. It looks something like this:

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

When using the run task the classpath is the value of sourceSets.main.runtimeClasspath. This value includes both directories and this works because of the way the classpath works. You can think of it like the classpath is just one giant module.

This doesn't work, however, when using the modulepath because technically the files inside resources do not belong to the module that's inside classes. We need a way to tell the module system that resources is part of the module. Luckily, there's --patch-module. This option will (quoted from java --help-extra):

override or augment a module with classes and resources in JAR files or directories.

And has the following format (I'm assuming the ; separator is platform dependent):

--patch-module <module>=<file>(;<file>)*

To allow your module to access it's own resources simply configure your run task like so:

run {
    input.property('moduleName', moduleName)
    doFirst {
        jvmArgs = [
                '--module-path', classpath.asPath,
                '--patch-module', "$moduleName=" + files(sourceSets.main.output.resourcesDir).asPath,
                '--module', "$moduleName/$mainClassName"
        ]
        classpath = files()
    }
}

This is how I've been doing it and it has worked out pretty well so far.


But how do you allow external modules to access resources from your module when launching the application from Gradle? This gets a little more involved.

When you want to allow external modules to access resources your module must opens (see Eng.Fouad's answer) the resource's package to at least the reading module (this only applies to encapsulated resources). As you've discovered, however, this leads to compilation warnings and runtime errors.

  1. The compilation warning is because you are trying to opens a package that doesn't exist according the the module system.
    • This is to be expected since the resources directory is not included when compiling by default (assuming a resource-only package).
  2. The runtime error is because the module system cannot find the package you have declared an opens directive for.
    • Again, assuming a resource-only package.
    • This occurs even with the --patch-module option mentioned above. I guess that the module system does some integrity checking before applying the patch.

Note: By "resource-only" I mean packages that have no .java/.class files.

To fix the compilation warning you just have to use --patch-module again inside the compileJava task. This time you'll use the resources' source directories rather than the output directory.

compileJava {
    inputs.property('moduleName', moduleName)
    doFirst {
        options.compilerArgs = [
                '--module-path', classpath.asPath,
                '--patch-module', "$moduleName=" + files(sourceSets.main.resources.srcDirs).asPath,
                '--module-version', "$version"
        ]
    }
}

For the runtime error there are a couple of options. The first option is to "merge" the resources output directory with the output directory for the classes.

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

jar {
    // I've had bad experiences when "merging" output directories
    // where the jar task ends up creating duplicate entries in the JAR.
    // Use this option to combat that.
    duplicateStrategy = DuplicatesStrategy.EXCLUDE
}

The second option is to configure the run task to execute the JAR file rather than the exploded directory(ies). This works because, like the first option, it combines the classes and resources into the same place and thus the resources are part of the module.

run {
    dependsOn += jar
    inputs.property('moduleName', moduleName)
    doFirst {
        // add JAR file and runtime classpath. The runtime classpath includes the output of the main source set
        // so we remove that to avoid having two of the same module on the modulepath
        def modulepath = files(jar.archivePath) + (sourceSets.main.runtimeClasspath - sourceSets.main.output)
        jvmArgs = [
                '--module-path', modulepath.asPath,
                '--module', "$moduleName/$mainClassName"
        ]
        classpath = files()
    }
}

Both of these options can be used in place of using --patch-module in the run task (explained in the first part of this answer).


As a bonus, this is how I've been adding the --main-class attribute to my modular JARs:

jar {
    inputs.property('mainClassName', mainClassName)
    doLast {
        exec {
            executable = 'jar'
            args = [
                    '--update', '--file', "$archivePath",
                    '--main-class', "$mainClassName"
            ]
        }
    }
}

This allows you to use java -m module.name rather than java -m module.name/some.package.Main. Also, if the run task is configured to execute the JAR you can change:

'--module', "$moduleName/$mainClassName"

To:

'--module', "$moduleName"

P.S. If there's a better way to do this please let me know.

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");
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!