Load a Resource from another Module in Java (With Maven)

Trying to put some resources on their own module on a Java 13 project, I ran into some issues where I could not load the resource. The idea was to reuse these resources (configs, styles, etc.) on multiple modules of my project.

Given a simple Maven project:

  • parent
  • app
  • config

Where module app depends on (requires) module configs.

Even though I am using Java 13 in this example, this should apply to a Java 11+ project.

Using Java 13, no module configured

When you have a Maven project without any module-info.java file, the good old classpath mechanism (as opposed to java modules) is used.

File structure:

.
├── app
│   ├── pom.xml
│   ├── src
│   │   ├── main
│   │   │   ├── java
│   │   │   │   └── org
│   │   │   │       └── example
│   │   │   │           └── app
│   │   │   │               └── Main.java
├── configs
│   ├── pom.xml
│   ├── src
│   │   ├── main
│   │   │   ├── java
│   │   │   └── resources
│   │   │       └── configs
│   │   │           └── app.ini
└── pom.xml
// Main.java
package org.example.app;

import java.io.IOException;
import java.net.URL;

public class Main {
  public static void main(String[] args) throws IOException {
    System.out.printf("Hello, let me read the config!%n");

    URL url = Main.class.getResource("/configs/app.ini");
    String content = new String(url.openStream().readAllBytes());
    System.out.println(content);
  }
}

This results in:

Hello, let me read the config!
value=test

This actually looks good. What happen if we throw some Java Module System into the mix?

Using Java 13, with modules

Our Java modules will reflect our Maven modules:

// app/src/main/java/module-info.java
module org.example.app {
  requires org.example.configs;
}

// configs/src/main/java/module-info.java
module org.example.configs {
}

On module names: I don’t have name my modules org.example.app, you could just name it app. I believe it’s a good practice to have some kind of qualifier to differenciate your module over modules outside your project. It’s kind of the same reasoning that we used to use to name java packages.

Now, running Main gives the following error:

Hello, let me read the config!
Exception in thread "main" java.lang.NullPointerException
	at app/org.example.app.Main.main(Main.java:12)

Oh no! Not quite what we expected. What happened here?

  • The resource has to be in a exported package.
  • Moreover, to load the resource with module.getResourceAsStream(), the package should be opened, not simply exported.
  • It’s OK if there is no class in src/main/java

Note how the configs/src/main/resources folder has changed.

.
├── app
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   ├── module-info.java
│       │   │   └── org
│       │   │       └── example
│       │   │           └── app
│       │   │               └── Main.java
│       │   └── resources
│       └── test
│           └── java
├── configs
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── module-info.java
│       │   └── resources
│       │       └── org.example.configs
│       │           └── app.ini
│       └── test
│           └── java
└── pom.xml

Some modifications have to be made to our module defnitions and Main.java:

// app/src/main/java/module-info.java
module org.example.app {
  requires org.example.configs;
}

// configs/src/main/java/module-info.java
module org.example.configs {
  opens org.example.configs;
}
// Main.java
package org.example.app;

import java.io.IOException;
import java.io.InputStream;

public class Main {
  public static void main(String[] args) throws IOException {
    System.out.printf("Hello, let me read the config!%n");

    InputStream stream = ClassLoader
      .getSystemResources("org.example.configs/app.ini")
      // Note that multiple matches are technically possible!
      .nextElement()
      .openStream();

    System.out.println(new String(stream.readAllBytes()));
  }
}

Now it’s all working as I wanted:

Hello, let me read the config!
value=test

In the end I, what I struggled with, was to figure out how to properly export my resource to another package.

Alternative way to read resource from a module

By using ClassLoader.getSystemResources it is possible to get multiple matches depending on what is on your module path.

You can aim for a resource in a particular module, using the following snippet.

package org.example.app;

import java.io.IOException;
import java.io.InputStream;

public class Main {
  public static void main(String[] args) throws IOException {
    System.out.printf("Hello, let me read the config!%n");

    Module module = ModuleLayer.boot()
      .findModule("org.example.configs")
      // Optional<Module> at this point
      .orElseThrow();
    InputStream stream = module.getResourceAsStream("org.example.configs/app.ini");

    System.out.println(new String(stream.readAllBytes()));
  }
}