1

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
davidalayachew
  • 1,279
  • 1
  • 11
  • 22
  • 1
    Simply adding `src/main/resources` to the module-path does not make the resources in that directory tree part of your module. You're actually recreating [a problem that Gradle used to have](https://stackoverflow.com/q/51864473/6395627) before JPMS support was added. When you package your JAR file you are including the resources in the JAR, making them part of the module. If you want to do the same before packaging the JAR, you'll either have to copy the resources into your `classes` directory or possibly make use of `--patch-module`. – Slaw Sep 02 '23 at 21:40
  • Note: If you were to build and execute your test project with modern Maven or Gradle, then you likely wouldn't have to do anything special. – Slaw Sep 02 '23 at 21:42
  • @Slaw ty vm! I definitely am aware that a build tool would work here, but I do not want to depend on that until I can do it without the build tool. I actually tried the classes copying and that didn't work. Let me try the --patch-module now. – davidalayachew Sep 02 '23 at 21:46
  • @Slaw I tried both options, but neither worked. I have edited my answer to detail the attempts. – davidalayachew Sep 02 '23 at 21:54
  • 1
    Note in my answer that `--patch-module` was used at _run-time_, not _compile-time_. – Slaw Sep 02 '23 at 23:07
  • @Slaw That clarified everything. And knowing that that is now possible, modules are so much easier. Plus, now I have exactly what I need in order to be able to finish up the answer to my other question. Thank you again for all of your help! – davidalayachew Sep 02 '23 at 23:16

1 Answers1

2

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.

    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
Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Work of art. --patch-module worked beautifully with no change other than a single command line option. When the time limit is up, I'm going to bounty this. – davidalayachew Sep 02 '23 at 23:15
  • I see your edit. Actually, I can list those resources, and then fetch them with a call to `getResourceAsStream`. I have to do it from the ModuleLayer, but it works. So, `ModuleReader` for walking the directory, and `ModuleLayer.boot().findModule(myModuleName).orElseThrow().getResourceAsStream(foundResource)` for actually fetching the file, after I found it by walking the directory. This behaviour is consistent across all execution environments for me. – davidalayachew Sep 03 '23 at 00:22
  • 1
    After you have the resource name, using `Class::getResource[AsStream](String)` should work just as well (just put a `/` at the front of the resource name to make it absolute). There's also `ClassLoader::getResource[AsStream](String)`, though I believe that requires the resource's package to be `opens` unconditionally. – Slaw Sep 03 '23 at 00:39
  • That's a very good point, ty! Yeah, now that I am in the module world for the first time, I am very eager to try out its variants of all of the methods, so I sometimes forget that modules play just as well with the old way of doing things too, to the point of complementing them. – davidalayachew Sep 03 '23 at 00:42
  • As a suggestion, it may be wise to mention that, for the `--patch-module` solution, that doing `--patch-module` doesn't actually move any files, it only pulls them in during runtime. I know the diagram implies that, but explicit is good. – davidalayachew Sep 03 '23 at 04:09