Loading classes and resources in Java 9
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?
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.
[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 packagenet.codetojoy.example.resources
- to beef up the jar,
net.codetojoy.example.Composer
is public (and exported, where applicable) - a simple
App
class that usesexample.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.
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.