Blog-Archiv

Montag, 4. Mai 2020

Java Modules and Resource Loading

When we load resources in Java, we normally use a resource-anchor class that is in the same package or archive as the resource. Such an archive can be the representation of a Maven module or a Java module (supported since version 9).

Why don't we simply open the resource as file? Because the file is available only since development of the Java app, after deployment the file is inside an archive and can not be opened by FileInputStream or similar. Thus we always use getClass().getResource(relativePath) to open any resource, because this works in both cases.

This Blog is about the fact that

resourceAnchorClass.getResource(relativePath) in Java since 9 works only when the caller of that method is in the same module as the resource-anchor class.

In other words: not only the resource-anchor class needs to be in the same module as the resource, also the caller of resourceAnchorClass.getResource() needs to be in that same module!

Example Modules

You need two Eclipse Java projects that you should name like your modules. Standard is the to use the package name of the module's main class. I chose following module names:

  • fri.module1
  • fri.module2

Here is the directory structure of both:

You see that these are Java 11 projects, so modules are supported. You establish a module by putting module-info.java in the module's root directory. Here is the one of fri.module1, which depends on no others (except the Java-built-in modules):

module fri.module1
{
    exports fri.module1;
}

You can see that this defines the name of the module, and states that it exposes the Java package fri.module1.

And here is module-info.java of fri.module2, which is a dependent module that uses fri.module1:

module fri.module2
{
    requires fri.module1;
}

Because this is a final application module, it exposes nothing.

Now we have two modules and can try out whether module2 can load resources from module1.

Example Resource and Classes

Module 1

This is what I put into fri.module1. First the ResourceAnchor.java class:

package fri.module1;

public class ResoureAnchor
{
}

Then the resource.txt file:

I am resource of module-one.

Module 2

Following is a utility class that can load a resource and turn its content into a plain text string (not usable for Unicode, just ASCII!). I called it ResourceUtil.java:

package fri.module2;

import java.io.InputStream;

public final class ResourceUtil
{
    public static String asText(Class<?> resourceAnchor, String fileName) throws Exception {
        InputStream inputStream = resourceAnchor.getResourceAsStream(fileName);
        StringBuilder sb = new StringBuilder();
        int c;
        while ((c = inputStream.read()) != -1)  {
            sb.append((char) c);
        }
        return sb.toString();
    }
    
    private ResourceUtil() {}   // do not instantiate
}

And here finally is the application that uses all these parts, called Main.java, we will call this class to see what happens:

package fri.module2;

import fri.module1.ResoureAnchor;

public final class Main
{
    public static void main(String[] args) throws Exception {
        String text = ResourceUtil.asText(ResoureAnchor.class, "resource.txt");
        System.out.println(text);
    }
    
    private Main() {}   // do not instantiate
}

This loads a resource from another module and prints its content to the console. In Eclipse you can execute such a class via context menu "Run As" - "Java Application".

Result

When you run this app, you will see the following:


Exception in thread "main" java.lang.NullPointerException
 at fri.module2/fri.module2.ResourceUtil.asText(ResourceUtil.java:11)
 at fri.module2/fri.module2.Main.main(Main.java:8)

So this failed in ResourceUtil on line 11 where the input-stream gets read. That means resourceAnchor.getResourceAsStream(fileName) returned null. We have no evidence why. This is how developers spend their time :-)

Fix

I guess that this has something to do with modules. So I move the ResourceUtil class into fri.module1. In Eclipse you can do that quickly with context menu "Refactor" - "Move". The modules look like the following now:

You see that ResourceUtil now is in fri.module1. The refactoring has put the new import into class Main, everything else is unchanged:

package fri.module2;

import fri.module1.ResourceUtil;
import fri.module1.ResoureAnchor;

public final class Main
{
    public static void main(String[] args) throws Exception {
        String text = ResourceUtil.asText(ResoureAnchor.class, "resource.txt");
        System.out.println(text);
    }
    
    private Main() {}   // do not instantiate
}

When you run the app now, you will see:


I am resource of module-one.

That means it worked now!

Conclusion

Using resourceAnchor.getResource(relativePath) in Java since 9 works only when the caller of that method is in the same module as the resource-anchor class.

This will not affect you as long as all your classes are in the "unnamed" module. But after module introduction you should put the code of ResourceUtil.asText() into ResourceAnchor, and never load a resource using just its anchor class (unless you are in the same module).




Keine Kommentare: