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
Here is my Main.java
.
package PackageName;
public class Main
{
public static void main(String[] args) throws Exception
{
new Main();
}
public Main() throws Exception
{
System.out.println("0 - " + ModuleLayer.boot().findModule("ModuleName") .map(o -> o.getClassLoader().getResource("PackageName/Main.class")));
System.out.println("1 - " + ModuleLayer.boot().findModule("ModuleName") .map(o -> o.getClassLoader().getResource("ResourceParentFolder")));
System.out.println("2 - " + ModuleLayer.boot().findModule("ModuleName") .map(o -> o.getClassLoader().getResource("ResourceParentFolder/ResourceSubFolderA")));
System.out.println("3 - " + ModuleLayer.boot().findModule("ModuleName") .map(o -> o.getClassLoader().getResource("/ResourceParentFolder")));
System.out.println("4 - " + ModuleLayer.boot().findModule("ModuleName") .map(o -> o.getClassLoader().getResource("/ResourceParentFolder/ResourceSubFolderA")));
System.out.println("4b - " + ModuleLayer.boot().findModule("ModuleName") .map(o -> o.getPackages()));
System.out.println("5 - " + ModuleLayer.boot().modules());
System.out.println("6 - " + ModuleLayer.boot().parents());
System.out.println("7 - " + ModuleLayer.boot().toString());
System.out.println("8 - " + Main.class.getClassLoader().getResource("ResourceParentFolder"));
System.out.println("9 - " + Main.class.getClassLoader().getResource("/ResourceParentFolder"));
System.out.println("10 - " + Main.class.getClassLoader().getResource("ResourceParentFolder/ResourceSubFolderA"));
System.out.println("11 - " + Main.class.getClassLoader().getResource("/ResourceParentFolder/ResourceSubFolderA"));
System.out.println("12 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("ResourceParentFolder"));
System.out.println("13 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("/ResourceParentFolder"));
System.out.println("14 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("ResourceParentFolder/ResourceSubFolderA"));
System.out.println("15 - " + Main.class.getClassLoader().getSystemClassLoader().getResource("/ResourceParentFolder/ResourceSubFolderA"));
}
}
module ModuleName
{
requires java.base;
}
Here is my shell script which performs compilation, executes it, creates a modular jar, executes that, and then calls jpackage
.
echo "STARTING TO COMPILE MODULAR SOURCE CODE"
javac \
--module-source-path=src/main/java \
--module=ModuleName \
-d classes
echo "STARTING TO RUN MODULAR SOURCE CODE"
java \
--module-path="classes" \
--module=ModuleName/PackageName.Main
echo "STARTING TO BUILD A MODULAR JAR"
jar \
--verbose \
--create \
--file run/executable/jar/ProjectName.jar \
--main-class PackageName.Main \
-C classes/ModuleName .
echo "STARTING TO RUN A MODULAR JAR"
java \
--module-path="run/executable/jar" \
--module=ModuleName/PackageName.Main
echo "STARTING TO RUN JPACKAGE"
jpackage \
--verbose \
--type msi \
--name ProjectName \
--input src/main/resources \
--install-dir davidalayachew_applications/ProjectName \
--vendor "David Alayachew" \
--win-dir-chooser \
--module-path run/executable/jar \
--module ModuleName/PackageName.Main \
--win-console \
--java-options "--enable-preview" \
--dest run/executable/installer
As you can see, it creates a few directories.
Now, the output during executions is where my frustration is.
Here is the output when running immediately after compiling.
STARTING TO RUN MODULAR SOURCE CODE
0 - Optional[file:**ignore**/ProjectName/classes/ModuleName/PackageName/Main.class]
1 - Optional[file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder]
2 - Optional[file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder/ResourceSubFolderA]
3 - Optional.empty
4 - Optional.empty
4b - Optional[[PackageName]]
5 - [**ignore**]
6 - []
7 - **ignore**
8 - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder
9 - null
10 - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder/ResourceSubFolderA
11 - null
12 - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder
13 - null
14 - file:**ignore**/ProjectName/src/main/resources/ResourceParentFolder/ResourceSubFolderA
15 - null
Ok, it found the folders. And since it is java.net.URLConnection
, I can traverse the directory by modifying the URL
. So, things worked running the code immediately after compile.
Here is the output when running the created jar file.
STARTING TO RUN A MODULAR JAR
0 - Optional[jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/PackageName/Main.class]
1 - Optional[jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/]
2 - Optional[jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/ResourceSubFolderA/]
3 - Optional.empty
4 - Optional.empty
4b - Optional[[ResourceParentFolder.ResourceSubFolderB, ResourceParentFolder.ResourceSubFolderA, ResourceParentFolder, PackageName]]
5 - [**ignore**]
6 - []
7 - **ignore**
8 - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/
9 - null
10 - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/ResourceSubFolderA/
11 - null
12 - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/
13 - null
14 - jar:file:///**ignore**/ProjectName/run/executable/jar/ProjectName.jar!/ResourceParentFolder/ResourceSubFolderA/
15 - null
Ok, a different type of connection, but we still found the same folders. I will likely have to do URL
construction a little differently, but the path forward is fairly clear.
Now, here is what happens after I run my newly installed jpackage
version of my application.
0 - Optional[jrt:/ModuleName/PackageName/Main.class]
1 - Optional.empty
2 - Optional.empty
3 - Optional.empty
4 - Optional.empty
4b - Optional[[ResourceParentFolder, PackageName, ResourceParentFolder.ResourceSubFolderA, ResourceParentFolder.ResourceSubFolderB]]
5 - [module ModuleName, module java.base]
6 - []
7 - ModuleName, java.base
8 - null
9 - null
10 - null
11 - null
12 - null
13 - null
14 - null
15 - null
Nothing can be found aside from the .class
file I put as a sanity check.
So, I opened up the installation directory, and I see that my resources are definitely there. The whole directory, subdirectories, and files. Everything inside of and including ResourceParentFolder
from my tree structure has been copied over to the C:/Program File/davidalayachew_applications/ProjectName/app folder.
I tried a laundry list of configurations, like prepending the getResource
parameters with app, but that was no good. I have cycled through >100 permutations, and truthfully, I have forgotten most of them.
Can someone give me a pointer on how to get my resources to be accessible to my application after calling jpackage? I specifically need to be able to see the contents of a folder. Not just fetching individual files.
You should not use .getResource()
or .getResourceAsStream()
to walk directories or view their contents. That was never part of their design. They are only meant to fetch files, not scan or see the contents of the directories.
That was the source of my bug. Yes, I can simply view the folder contents by doing .getResource()
or .getResourceAsStream()
, but that works for plain folders and jar files only because plain folders (obviously) have that functionality built in and jars can halfway simulate the same thing. But trying to do that for an executable file (.exe file), will just plain not work. Here is the real way to do it.
Modules are a beautiful thing. If you want to read the contents of your Module, just get an instance of a java.lang.module.ModuleReader
. From there, you can get the directory searching abilities I was trying to get with getResource()
or .getResourceAsStream()
. Just call ModuleReader.list()
. Please note, it implements AutoCloseable, so use Try-with-resources. I am also following suit with what @Slaw did in this answer. https://stackoverflow.com/a/77030323/10118965
package PackageName;
public class Main
{
public static void main(String[] args) throws Exception
{
new Main();
}
public Main() throws Exception
{
var module = Main.class.getModule();
var reference = module.getLayer()
.configuration()
.findModule(module.getName())
.orElseThrow()
.reference();
try (var reader = reference.open()) {
reader.list().forEach(System.out::println);
}
}
From there, I can scan my directories and see the contents of my modules, and all I need to do is use a ModuleReader
. I can pass the values into a getResource
or getResourceAsStream
call and it should work just fine. Ideally, one of the Module specific ones.
So, let's apply these new strategies to my problem. I have already pasted @Slaw 's Main.java
above in this answer. Here are my new shell scripts.
First is the raw compile command. That remains unchanged for me.
javac \
--module-source-path=src/main/java \
--module=ModuleName \
-d classes
Next is running the modular source code. This one needs to change slightly.
java \
--module-path="classes" \
--module=ModuleName/PackageName.Main \
--patch-module ModuleName=src/main/resources
As you can see, I added a --patch-module
command line there. Special thanks (yet again!) to @Slaw 's answer -- https://stackoverflow.com/a/77030323/10118965
The --patch-module
command has documentation here -- https://docs.oracle.com/en/java/javase/20/docs/specs/man/java.html (Ctrl+F "--patch-module")
This command is what you use to insert resources into modules. All you do is specify the module you want to "patch" on the left side of the equals, and then on the right, put the directory that you want to copy over, recursively.
So, in my case, I want to copy the contents of src/main/resources (not the directory itself, but its contents) into my module. So, I just call the above command when doing my run command, and things are good!
I would encourage reading @Slaw 's answer, it goes into more detail.
Next is the script to create my jar file. This one will also change slightly.
jar \
--verbose \
--create \
--file run/executable/jar/ProjectName.jar \
--main-class PackageName.Main \
-C classes/ModuleName . \
-C src/main/resources .
The only new addition is the second -C
call. The jar command does not have the --patch-module
option, but you can think of the -C
option as essentially an alias for --patch-module
. By doing this, I achieve the exact same effect (to my understanding).
Next is my run command for the jar. This one remains unchanged.
java \
--module-path="run/executable/jar" \
--module=ModuleName/PackageName.Main
The reason why it is unchanged is because -C
actually inserts the src/main/resources
into the jar. So, no need to insert any special commands! Since we are ingesting the jar, then we are ingesting all of its contents too - which means that we get the resources too!
Finally, is the jpackage shell script. This one actually gets simpler!
jpackage \
--verbose \
--type msi \
--name ProjectName \
--install-dir davidalayachew_applications/ProjectName \
--vendor "David Alayachew" \
--win-dir-chooser \
--module-path run/executable/jar \
--module ModuleName/PackageName.Main \
--win-console \
--java-options "--enable-preview" \
--dest run/executable/installer
The only change here is that I removed the --input
option from the jpackage script. Special thanks to @VGR for pointing this out! --input
is now redundant because we already have the resources inside of our jar -- no need to manually insert it!
Now, when I run all of the above, things work out beautifully!
Here is the output when I run my modular code (not a jar).
module-info.class
PackageName/
PackageName/Main.class
ResourceParentFolder/
ResourceParentFolder/ResourceSubFolderA/
ResourceParentFolder/ResourceSubFolderA/Resource1A.png
ResourceParentFolder/ResourceSubFolderA/Resource2A.png
ResourceParentFolder/ResourceSubFolderA/Resource3A.png
ResourceParentFolder/ResourceSubFolderB/
ResourceParentFolder/ResourceSubFolderB/Resource1B.png
ResourceParentFolder/ResourceSubFolderB/Resource2B.png
ResourceParentFolder/ResourceSubFolderB/Resource3B.png
Remember, I changed my output command to simply list the locations of all of my module contents. If I wanted to fetch any of these resources, I would just call my getResource
or getResourceAsStream
methods.
But to be absolutely sure, I can instead, make a call to actually fetch one of the resources.
Here is my new Main.java
.
package PackageName;
public class Main
{
public static void main(String[] args) throws Exception
{
new Main();
}
public Main() throws Exception
{
try
(
final java.lang.module.ModuleReader reader =
ModuleLayer
.boot()
.configuration()
.findModule("ModuleName")
.get()
.reference()
.open()
)
{
final String firstResource =
reader
.list()
.filter(each -> each.contains("ResourceParentFolder/ResourceSubFolderA/Resource1A.png"))
.findFirst()
.orElseThrow()
;
final var myModule =
ModuleLayer
.boot()
.findModule("ModuleName")
.orElseThrow()
;
final var inputStream = myModule.getResourceAsStream(firstResource);
final java.awt.Image image = javax.imageio.ImageIO.read(inputStream);
System.out.println(image);
}
}
}
Now, instead of printing all resources with a Stream<String>
, I simply filter down to the one I want, and then load it.
And when I do it like this, it all just works!
Here is the output for my run command (no jar).
BufferedImage@3ecd23d9: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@569cfc36 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 96 height = 88 #numDataElements 4 dataOff[0] = 3
It successfully fetched the image! I can take the returned object and put into my GUI, and everything works just fine!
Here is the output for my run command (yes jar).
BufferedImage@de0a01f: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@4c75cab9 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 96 height = 88 #numDataElements 4 dataOff[0] = 3
A 1-to-1 match!
And here is the output for my jpackage installation.
BufferedImage@1f554b06: type = 6 ColorModel: #pixelBits = 32 numComponents = 4 color space = java.awt.color.ICC_ColorSpace@694e1548 transparency = 3 has alpha = true isAlphaPre = false ByteInterleavedRaster: width = 96 height = 88 #numDataElements 4 dataOff[0] = 3
It's a match! I now have a solution for querying my module and fetching resources from it! And the solution is uniform across all execution contexts -- whether I am running some just-compiled .class files, or I am running a jar file, or I am executing the application as a jpackage created executable binary, I am set!