27

So far until non-modularized java, you would simply put a file in src/main/java/resources make sure it is in classpath and then load it with

file = getClass().getClassLoader().getResourceAsStream("myfilename"); 

from pretty much anywhere in the classpath.

Now with modules, the plot thickens.

My project setup is the following:

module playground.api {
    requires java.base;
    requires java.logging;
    requires framework.core;
}

Config file is placed inside src/main/resources/config.yml.

Project is run with

java -p target/classes:target/dependency -m framework.core/com.framework.Main

Since the main class does not reside in my own project, but in an external framework module, it can't see config.yml. Now the question is, is there a way to somehow put my config file into the module or open it up? Do I have to change the way file is loaded by framework upstream?

I tried using "exports" or "opens" in module-info but it wants to have a package name, not a folder name.

How to achieve this in best practical way so it would work as in Java 8 and with as little changes as possible?

Braiam
  • 1
  • 11
  • 47
  • 78
cen
  • 2,873
  • 3
  • 31
  • 56
  • 3
    Does `com.framework.Main` read resources using `Class.getResource`? – Naman Oct 21 '17 at 10:55
  • 5
    If code in a module needs to access one of its own resources then the Class.getResourceXXX methods should be used (the parameter name is a resource name, not a file name btw). If the resource is in another module and you have the Module object then you can use Module.getResourceAsStream. If you want to search the module path and class path then ClassLoader.getResourceXXX will work as before but the Module needs to open the package containing the resource. Resources in the top-most directory or META-INF/* are not encapsulated so ClassLoader.getResource will work. – Alan Bateman Oct 21 '17 at 11:41
  • 2
    Related [how-to-let-an-automatic-module-find-its-own-resources-in-java-9](https://stackoverflow.com/questions/43573809/how-to-let-an-automatic-module-find-its-own-resources-in-java-9) – Naman Oct 22 '17 at 11:40
  • @nullpointer yes it does – cen Oct 22 '17 at 16:38
  • @Alan Bateman how do I open up the resource files, specifically `src/main/resources` if it's not a package? I guess I could put it in a package but that seems like an anti-pattern. That is the first part of my confusion. And second, by top level you mean which folder? I also tried putting my config file in META-INF but no dice. – cen Oct 22 '17 at 16:51
  • 1
    @cen *I tried using "exports" or "opens" in module-info*... could you share what you tried and didn't work there. – Naman Oct 22 '17 at 17:03
  • I just naively tried to `opens src/main/resources`, doesn't compile ofc. – cen Oct 22 '17 at 17:21
  • 2
    src/main/resources/config.yml is the file path in the src tree, it is not the resource name. Look in the JAR file, is config.yml in the top-level directory of the JAR file? If so then the framework with locate it with ClassLoader.getResourceXXX as before. – Alan Bateman Oct 22 '17 at 17:39
  • @Alan Bateman it is inside the jar, also in `target/classes` but it does not work. It does work if I add `-cp target/classes` though. But should one use -cp when using jigsaw? It feels like I shouldn't for some reason. – cen Oct 22 '17 at 18:10
  • 4
    Right, you shouldn't add the module classes to the class path with `-cp`. Can you instead add `--add-moduels playground.api` to the command line. The initial module (the module you specify to -m) is com.framework.Main and I assume that nobody requires playground.api so it's not being resolved (you can quickly check this by adding `--show-module-resolution` to trace resolution at startup). – Alan Bateman Oct 22 '17 at 18:21
  • 1
    Bingo! That did it and makes sense. – cen Oct 22 '17 at 18:24
  • This could be useful: https://stackoverflow.com/a/53184054/597657 – Eng.Fouad Nov 07 '18 at 05:34

3 Answers3

14
// to scan the module path
ClassLoader.getSystemResources(resourceName)

// if you know a class where the resource is
Class.forName(className).getResourceAsStream(resourceName)

// if you know the module containing the resource
ModuleLayer.boot().findModule(moduleName).getResourceAsStream(resourceName)

See a working example below.


Given:

.
├── FrameworkCore
│   └── src
│       └── FrameworkCore
│           ├── com
│           │   └── framework
│           │       └── Main.java
│           └── module-info.java
└── PlaygroundApi
    └── src
        └── PlaygroundApi
            ├── com
            │  └── playground
            │      └── api
            │          └── App.java
            ├── config.yml
            └── module-info.java

Main.java could be

package com.framework;

import java.io.*;
import java.net.URL;
import java.util.Optional;
import java.util.stream.Collectors;

public class Main {
    public static void main( String[] args )
    {
        // load from anywhere in the modulepath
        try {
            URL url = ClassLoader.getSystemResources("config.yml").nextElement();
            InputStream is = url.openStream();
            Main.read(is);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // load from the the module where a given class is
        try {
            InputStream is = Class.forName("com.playground.api.App").getResourceAsStream("/config.yml");
            Main.read(is);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }

        // load from a specific module
        Optional<Module> specificModule = ModuleLayer.boot().findModule("PlaygroundApi");
        specificModule.ifPresent(module -> {
            try {
                InputStream is = module.getResourceAsStream("config.yml");
                Main.read(is);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });
    }

    private static void read(InputStream is) {
        String s = new BufferedReader(new InputStreamReader(is)).lines().collect(Collectors.joining("\n"));
        System.out.println("config.yml: " + s);
    }
}

And you would launch with

java --module-path ./FrameworkCore/target/classes:./PlaygroundApi/target/classes \
     --add-modules FrameworkCore,PlaygroundApi \
       com.framework.Main

To clone this example: git clone https://github.com/j4n0/SO-46861589.git

Naman
  • 27,789
  • 26
  • 218
  • 353
Jano
  • 62,815
  • 21
  • 164
  • 192
  • 1
    See [App.java:20](https://github.com/j4n0/SO-46861589/blob/master/PlaygroundApi/src/PlaygroundApi/com/playground/api/App.java#L20) to load a resource from the same module. – Jano Oct 22 '17 at 12:56
  • I'll try this just to see if it works however, I have a problem with this. Ideally, framework module should not care about which module contains the resource file nor should it care that the module in question is called "PlaygroundApi". If I make 100 projects, each one will have a different module name which means I need to make the module name in framework code configurable. Kinda painful but doable I guess. – cen Oct 22 '17 at 16:45
  • 1
    The answer is a bit misleading as there the framework should only need to the use the Module API then it wants to locate a resource in a specific module. As note in one of the early comments, a resource is only encapsulated if it's in a module package, in which case the module-info.java can open the package to allow the framework locate the resource. – Alan Bateman Oct 22 '17 at 17:45
  • Thanks. I split the code and added the Classloader for top resources anywhere in the module path. – Jano Oct 22 '17 at 20:17
  • 1
    The intro section in your answer is good now. I guess someone should write an answer to summarize the real issue now as it turned out not to be a resource encapsulation issue. – Alan Bateman Oct 23 '17 at 09:47
  • Note that ther first one is a [sytem class loader](https://stackoverflow.com/questions/6608795/what-is-the-difference-between-class-getresource-and-classloader-getresource) and so if you are using an abolute `resourceName` you have to remove the leading `/`. – Itchy Oct 11 '21 at 14:41
7

While you are using the java command to launch an application as follows:-

java -p target/classes:target/dependency -m framework.core/com.framework.Main 
  • you are specifying the modulepath using the option -p aternate for --module-path which would look up into target/classes and target/dependency for your modules.

  • Alongside, using -m alternate for --module specifies the initial module to resolve with the name framework.core and constructs the module graph with the main class to execute explicitly listed as com.framework.Main.

Now, the problem here seems to be that the module framework.core doesn't requires or read playground.api module because of which the module graph doesn't include the desired module consisting of the actual resource config.yml.

As suggested by @Alan, a good way to list out the module resolution output during startup is to make use of the --show-module-resolution option.


I just naively tried to opens src/main/resources, doesn't compile ofc

Since the resource in your module is at the root level, it is, therefore, not encapsulated and does not need to be opened or exported to any other module.

In your case, you just need to make sure that the module playground.api ends up in the module graph and then the resource would be accessible to the application. To specify root modules to resolve in addition to the initial module, you can make use of the --add-modules option.


Hence the overall solution to work for you along with some debugging shall be :

java --module-path target/classes:target/dependency 
     --module framework.core/com.framework.Main
     --add-modules playground.api
     --show-module-resolution
Naman
  • 27,789
  • 26
  • 218
  • 353
0

When running in a named module, ClassLoader#getResource has very surprising behavior. ClassLoader#getResourceAsStream is just as troublesome. I ran into this myself while upgrading an application to named modules.

If your code is in a named module, and you access a resource via ClassLoader#getResource, the package of that resource must be opened unconditionally. Otherwise, you will not be able to retrieve the resource, even if the resource is in the same module.


This is different behavior from Class#getResource - note the distinction. Class#getResource is straightforward and does not have this nasty surprise.

Everything said about getResource applies to the getResourceAsStream methods, too.

For this reason, I suggest always using the getResource methods on Class and NOT the ones on ClassLoader.

Also see my answer to What is the difference between Class.getResource and ClassLoader.getResource

A248
  • 690
  • 7
  • 17