Search code examples
javajarexecutable-jarjava-modulejava-platform-module-system

ModuleLayer can't find my resources when running my modular program, but it can for my modular jar


I have the following project structure.

ProjectName
|
|---src
    |
    |---main
        |
        |---java
        |   |
        |   |---ModuleName
        |       |
        |       |---module-info.java
        |       |
        |       |---PackageName
        |           |
        |           |---Main.java
        |
        |---resources
            |
            |---ResourceParentFolder
                |
                |---ResourceSubFolderA
                |   |
                |   |---Resource1A.png
                |   |---Resource2A.png
                |   |---Resource3A.png
                |
                |---ResourceSubFolderB
                    |
                    |---Resource1B.png
                    |---Resource2B.png
                    |---Resource3B.png

I have a shell script that compiles the code, and then runs that code.

javac                                        \
        --module-source-path="src/main/java" \
        --module=ModuleName                  \
        -d classes

java                                               \
        --module-path="classes;src/main/resources" \
        --module=ModuleName/PackageName.Main

I also have a shell script that turns my compiled code into a modular jar, and then runs that jar.

jar                                               \
        --verbose                                 \
        --create                                  \
        --file run/executable/jar/ProjectName.jar \
        --main-class PackageName.Main             \
        -C classes/ModuleName .                   \
        -C src/main/resources .

java                                         \
        --module-path="run/executable/jar"   \
        --module=ModuleName/PackageName.Main

In my main method, I have a call to java.lang.module.ModuleReader, specifically to its list() method, that allows me to traverse my module and its contents.

I am able to see the contents of my ResourceParentFolder if I take my jar file and try to run it, but the call to list() only returns the .class files when I am only running my compiled code. Is this because my module is misconfigured? Or is this simply unsupported functionality?

Again, ModuleReader.list() returns a recursive list of the contents of my source code and my resource folder when run as a jar, but it only returns the source code when run as compiled code. How do I get the compiled code to also populate the ModuleReader.list()? Or is that just not supported functionality unless it is in a jar or something?

And to be clear, I am well aware that there are a million and one other ways to fetch a resource. But I want to know if it is possible to do it the way I requested above. And if not, then why?

EDIT -- detailing some of my failed attempts.

I tried copying the src/main/resources directory into classes, the location of my module before it turns into a jar. Unfortunately, nothing got picked up by it.

I also tried to do --patch-modules, but that also failed, but with an error.

error: no source files

Here is the command that I used.

javac   --patch-module ModuleName=src/main/resources

Solution

  • Your attempt to add src/main/resources to the module path won't work as you expect. Simply adding directories to the module-path does not make their contents part of your module. You need to either:

    1. Have the classes and the resources in the same location, whether that's a directory or a JAR file. This makes the resources part of the module. And note this is why your JAR file works as expected, because you've packaged the classes and the resources in the same JAR file.

      This approach is essentially how it's "supposed to be done".

    2. Use --patch-module at run-time. Note this approach does not require moving the resource files anywhere.

      You would additionally use this argument at compile-time if you were trying to opens a resource-only package; otherwise, a warning is emitted saying the package doesn't exist. Though if I recall correctly, you'll get an error at run-time in this scenario (where an opens package is not inherently part of the module) even with a --patch-module argument.

    All that said, I do recommend using a build tool (e.g., Maven, Gradle, etc.) for non-trivial Java projects. They will typically handle all this for you.


    Example

    Here is an example of both approaches discussed above. Note all commands were executed in the project directory on Windows 10 using PowerShell Core 7.2.13 and Java 20.0.1.

    While the output below lists resource directories as well as actual resources, that is not guaranteed. In fact, if I'm not mistaken, those same directories will not be listed when the module is packaged in a run-time image (via jlink / jpackage).

    Source Code

    The source code is the same in both approaches.

    module-info

    module app {}
    

    sample.Main

    package sample;
    
    public class Main {
    
        public static void main(String[] args) throws Exception {
            var module = Main.class.getModule();
            var reference = module.getLayer()     // ModuleLayer
                    .configuration()              // Configuration
                    .findModule(module.getName()) // Optional<ResolvedModule>
                    .orElseThrow()                // ResolvedModule
                    .reference();                 // ModuleReference
            try (var reader = reference.open()) {
                reader.list().forEach(System.out::println);
            }
        }
    }
    

    Directory structure

    <PROJECT-DIR>
    |
    \---src
        \---main
            +---java
            |   \---app
            |       |   module-info.java
            |       |
            |       \---sample
            |               Main.java
            |
            \---resources
                \---app
                    \---foo
                            res.txt
    

    Note I added an app "module directory" under src/main/resources simply to mirror what was done under src/main/java (which is needed if you want to use --module-source-path and --module with javac). This is not necessary, though not having it would require changing the commands used below slightly (just changes to paths).

    Approach 1 - Merge Classes and Resources

    Commands

    & javac --module-source-path src\main\java --module app -d build\classes
    & robocopy src\main\resources build\classes /S > NUL
    & java --module-path build\classes --module app/sample.Main
    

    Output

    foo/
    foo/res.txt      
    module-info.class
    sample/
    sample/Main.class
    

    As you can see, the resource foo/res.txt (and even the directory foo/) was listed.

    Directory structure (after running commands)

    <PROJECT-DIR>
    |
    +---build
    |   \---classes
    |       \---app
    |           |   module-info.class   
    |           |
    |           +---foo
    |           |       res.txt
    |           |
    |           \---sample
    |                   Main.class      
    |
    \---src
        \---main
            +---java
            |   \---app
            |       |   module-info.java
            |       |
            |       \---sample
            |               Main.java   
            |
            \---resources
                \---app
                    \---foo
                            res.txt 
    

    Approach 2 - Use --patch-module

    Commands

    & javac --module-source-path src\main\java --module app -d build\classes
    & java --module-path build\classes --patch-module app=src\main\resources\app --module app/sample.Main
    

    Output

    module-info.class
    sample/
    sample/Main.class
    foo/
    foo/res.txt
    

    Again, you can see the resource foo/res.txt (and the directory foo/) was listed.

    Directory structure (after running commands)

    <PROJECT-DIR>
    |
    +---build
    |   \---classes
    |       \---app
    |           |   module-info.class
    |           |
    |           \---sample
    |                   Main.class
    |
    \---src
        \---main
            +---java
            |   \---app
            |       |   module-info.java
            |       |
            |       \---sample
            |               Main.java
            |
            \---resources
                \---app
                    \---foo
                            res.txt