Loading classes and resources post Java 9
Asked Answered
S

3

50

I was reading this article on InfoQ quoting Reinhold:

Developers can still use the Java class path in Java 9 for the Java runtime to search for classes and resource files. It's just that with Java 9's modules, developers no longer need the class path.

So now my question is: what is the proper Java 9 way to do the tasks listed above? How do you dynamically load e.g. an image (short of fiddling with relative paths)?

Even more interestingly, how would one check if a class is available and make a decision dynamically (e.g. check if Jackson is available and, if so, use it for JSON serialization and if not use something else)?

The article also mentions Spring Boot already supporting Java 9, and Spring Boot definitely does a lot of dynamic loading. So maybe someone knows the priece of code from Spring that I can look at?

Siddra answered 18/7, 2017 at 12:25 Comment(0)
A
97

First, to set the record straight, I neither said nor wrote the text quoted above. I’d never have put it that way. That’s just sloppy reporting on the part of the publications involved.

The most important thing to understand about class loading and resource lookup in Java 9 is that, at a fundamental level, they have not changed. You can search for classes and resources in the same way that you always have, by invoking Class::forName and the various getResource* methods in the Class and ClassLoader classes, regardless of whether your code is loaded from the class path or the module path. There are still three built-in class loaders, just as there were in JDK 1.2, and they have the same delegation relationships. Lots of existing code therefore just works, out-of-the-box.

There are some nuances, as noted in JEP 261: The concrete type of the built-in class loaders has changed, and some classes formerly loaded by the bootstrap class loader are now loaded by the platform class loader in order to improve security. Existing code which assumes that a built-in class loader is a URLClassLoader, or that a class is loaded by the bootstrap class loader, may therefore require minor adjustments.

A final important difference is that non-class-file resources in a module are encapsulated by default, and hence cannot be located from outside the module unless their effective package is open. To load resources from your own module it’s best to use the resource-lookup methods in Class or Module, which can locate any resource in your module, rather than those in ClassLoader, which can only locate non-class-file resources in the open packages of a module.

Adim answered 18/7, 2017 at 17:50 Comment(0)
K
6

[Edit: this answer was written prior to Mark's authoritative answer. I've revised mine to provide a simple example, available on GitHub.]

Per this video, class loading in Java 9 is unchanged.

As an example, let's say we have:

  • an example.jar that contains an image in the package net.codetojoy.example.resources
  • to beef up the jar, net.codetojoy.example.Composer is public (and exported, where applicable)
  • a simple App class that uses example.jar as a library and attempts to load the image from it

The relevant code in App:

static InputStream getResourceAsStream(String resource) 
    throws Exception {

    // Load net/codetojoy/example/resource/image.jpg
    // Assume net.codetojoy.example.Composer is public/exported
    // resource is 'resource/image.jpg'

    InputStream result = Composer.class.getResourceAsStream(resource);

    return result;
}   

Here are a few cases for example.jar in JDK 9:

Old-Fashioned, Non-Modular Jar

If example.jar is not a module, the code just works. Class loading is unchanged.

Modular Jar With Open Package

In this case, this is the module-info.java file:

module net.codetojoy.example {
    // export the Composer class
    exports net.codetojoy.example;

    // image is available
    opens net.codetojoy.example.resources;
}

In this case, the image can be loaded by the client, because the package is open.

Modular Jar Without Open Package

In this case, module-info.java is:

module net.codetojoy.example {
    // export the Composer class
    exports net.codetojoy.example;

    // package not opened: image not available
    // opens net.codetojoy.example.resources;
}

In this case, the image cannot be loaded, because of strong encapsulation: the module is protecting the image by not opening the package.

Full source here on GitHub.

Koosis answered 18/7, 2017 at 16:3 Comment(2)
Thanks, but what about those using an older language level where module-info.java isn't supported, but we're still running into walls with the resource loader? Is there any concession for developers looking to play nicely with JRE9 without yet adopting the new JDK9 language level?Aseity
Nevermind, my issue was the use of Foo.class.getClass().getResourceAsStream(...) versus Foo.class.getResourceAsStream(...). JDK9 seems to handle the former differently than JDK8, JDK7.Aseity
G
4

In addition to the existing answers I’d like to give an example of encapsulation rules for non-class-file resources for different resource directory names.

The specification for getResourceAsStream says that the resource is encapsulated if a package name is derived from its name.

So if the resource’s directory name is NOT a valid Java identifier, it is NOT encapsulated. Which means that if a module has a resource located under, for example, a directory named dir-1 (containing a non-valid character - in its name) it will always be accessible from outside of the module.

Here is an example of two Java modules (source code in GitHub). Module 1 consists of the following resource files:

├── dir-3
│   └── resource3.txt
├── dir1
│   └── resource1.txt
├── dir2
│   └── resource2.txt
└── root.txt

and module-info.java:

module module_one {
  opens dir1;
}

Module 2 requires Module 1 in module-info.java:

module module_two {
  requires module_one;
}

and has a sample main class for loading various resource files:

package module2;

import java.io.IOException;

public class Main {
  public static void main(String[] args) throws IOException {
    loadResource("root.txt", "From module's root directory");
    loadResource("dir1/resource1.txt", "From opened package `dir1`");
    loadResource("dir2/resource2.txt", "From internal package `dir2`");
    loadResource("dir-3/resource3.txt", "From directory `dir-3` with non-Java name");
  }

  public static void loadResource(String name, String comment) throws IOException {
    // module2 application class loader
    final var classLoader = Main.class.getClassLoader();
    try (var in = classLoader.getResourceAsStream(name)) {
      System.out.println();
      System.out.println("// " + comment);
      System.out.println(name + ": " + (in != null));
    }
  }
}

Running the class above gives the following output:



// From module's root directory
root.txt: true

// From opened package `dir1`
dir1/resource1.txt: true

// From internal package `dir2`
dir2/resource2.txt: false

// From directory `dir-3` with non-Java name
dir-3/resource3.txt: true

As you can see the resource file from the root directory and from the dir-3 directory are not encapsulated, therefore Module 2 can load them.

The package dir1 is encapsulated but unconditionally opened. Module 2 can load it as well.

The package dir2 is encapsulated and not opened. Module 2 cannot load it.

Note that Module 2 cannot contain their own resources under dir1 and dir2 directories because they are already encapsulated in Module 1. If you try adding dir1 you will get the following error:

Error occurred during initialization of boot layer
java.lang.LayerInstantiationException: Package dir1 in both module module_one and module module_two

Here is a Flyway related issue for reference.

Graveclothes answered 5/6, 2021 at 18:53 Comment(1)
I can not agree, I am no expert in module system, but I could not load any resources from a moduleA when not using a class loader from this moduleA. Even within a qualified identifier it was not visible.Weekend

© 2022 - 2024 — McMap. All rights reserved.