Header background

What’s ahead with Java 9 & Project Jigsaw

With Java 9 finally released, it’s time to look at some of its new features. This post covers the most prominent (and most criticized) feature (i.e., Project Jigsaw).

Project Jigsaw splits the JDK into several modules and Java developers are encouraged to do the same in their code as well. Every module can, for example, be packaged into a jar file and shipped separately. All modules contain meta information compiled from a file called module-info.java. This meta information tells the JVM what packages within that module can be used by other modules, and what other modules are required.

Let’s implement a simple RMI application consisting of three modules: (1) a module defining the common interfaces, (2) one containing the server, and (3) another module containing the client. The interface of our simple application looks like this:

package com.dynatrace.deepthought;
import java.rmi.*;
public class DeepThought extends Remote {
   public abstract String getTheAnswer() throws RemoteException
}

To package this class into a module, we also need to write the module-info.java:

module deepthought.interfaces {
 exports com.dynatrace.deepthought;
 requires java.rmi;
}

This snippet defines three important things: (1) our module is called deepthought.interfaces, i.e., our module can be referenced via this name, (2) we export the package com.dynatrace.deepthought, i.e., every module referencing our module can only use classes from that package, and (3) our module uses classes from the module java.rmi. The “requires” statement is necessary to use classes from the java.rmi packages, even though we have imported them. In addition to that module dependency, our module has also an implicit dependency to the module java.base. We do not have to specify this dependency because every module implicitly requires java.base. The java.base module contains all classes in java.lang, java.util, java.io, java.net, and many others.

To complete our application, we still have to implement the server and the client. Let’s define a simple client:

package com.dynatrace.deepthought.client;

import java.rmi.*;
import com.dynatrace.deepthought.*;
public class DeepThoughtClient {
 public static void main(String[] args) {
  Registry registry = LocateRegistry.getRegistry(“127.0.0.1”, 1099);
  DeepThought dt = (DeepThought) registry.lookup(“DeepThought”);
  System.out.println(dt.getTheAnswer());
 }
}

And let’s also define another module-info.java:

module deepthought.client {
 exports com.dynatrace.deepthought.client;
 requires java.rmi;
 requires deepthought.interfaces;
}

Please note that we again require the modules java.rmi again and deepthought.interfaces because we need access to the interface. Also, a package must not be split across multiple modules, hence we need to put our client into another package.

In a similar fashion, we can implement the server part:

package com.dynatrace.deepthought.server;
import java.rmi.*;
import com.dynatrace.deepthought.*;
public class DeepThoughtServer {

 public static void main(String[] args) {
  Registry registry = LocateRegistry.createRegistry(1099);
  DeepThought dt = new DeepThought() {
   public String getTheAnswer() { return “42”; }
  };
  UnicastRemoteObject.export(dt, 0);
  registry.bind(“DeepThought”, dt);
 }

}
module deepthought.server {
 exports com.dynatrace.deepthought.server;
 requires java.rmi;
 requires deepthought.interfaces;
}

Compiling and running with modules is basically the same with the only difference is that instead of a classpath, you now need to supply a module path. This module path is, compared to the classpath, not a list of jars and directories where classes lie, but rather a directory, in which jars and directories are located. Every jar and directory is expected to contain exactly one module and the JVM will report an error if one of the jars or directories contain no or more than one module-info.

Classloading and Access Rules

Having implemented our first small example, we need to talk about what all that means for classloading. Running the code above, all modules are loaded by the same classloader, i.e., the system classloader. However, this does not mean that we automatically have access to all classes like in the past. Accessing a class (via reflection or directly) from an unexported package in a different module will lead to an IllegalAccessError. This means that modules are not a physical entity as such, but rather as a mechanism for classloaders to restrict access. Class resolution, though, works exactly the same as before.

As the JDK is also split into modules, starting with Java 9, consequently the JVM will restrict access to internal classes, e.g., all classes in sun.* or jdk.* packages. Granted, no application should use these classes because they are internal for a reason and often subject to change without notice. Sadly however, there are many applications and libraries out there using jdk internal classes. To not break them right away, the JVM will, for now, just log illegal access in these cases. This means the JVM will log a “loud” warning to stderr every time a class accesses an internal class. This log cannot be turned off, and, with Java 10, the access to these classes will finally be made impossible.

For those who want to venture into realms full of IllegalAccessErrors, you can already enforce the strict mode by adding the command line flag –illegal-access=deny.

Classloading the JDK

Previously, the JDK had three out-of-the-box classloaders: (1) the bootstrap classloader, (2) the extension classloader, and the (3) system classloader. The bootstrap classloader loaded the entire JDK, the extension classloader loaded some installed stuff from disk. The system classloader loaded your main class and served as parent classloader for most custom classloaders.

In Java 9, the extension classloader is dropped and a new classloader called the platform classloader is introduced. The only guarantee a JVM makes is that the java.base module is loaded by the bootstrap classloader and that the entire JDK is accessible via the platform classloader. This means that, assuming a JVM does not define any additional out-of-the-box classloaders, every JDK module will either be loaded by the bootstrap classloader or the platform classloader. For most applications, this will make no difference, but it will make things a little more difficult for APM vendors, such as Dynatrace.

Using Pre-Java-9 Libraries with Java 9

There are lot of libraries out there that have been compiled with Java 8 or earlier versions. This means that these libraries have no module definition. Does this mean we cannot use old libraries? Of course we can with the help of “unnamed modules”.

Modules are loaded via the “module path”, but we can still add jars or directories to the good old class path. As we have learned above, classloading is mostly the same, meaning that classes from these locations can be resolved without any problems. However, the classloader must still put them in some module. For this purpose, every classloader has its own “unnamed module”. All classes loaded from the classpath are put into that module, so they can access each other without problems. Furthermore, every module, has an implicit dependency to all other modules of the same classloader and all parent classloaders. This is necessary so that old libraries can still access classes such as java.lang.String (they are also part of module, remember?). If you want to access your legacy library from your new (modularized) code, you need to add a dependency to the unnamed module. Unfortunately, you cannot do that explicitly (it is called the “unnamed module” for reason). However, you can do that at run-time, e.g., in a static constructor:


static {
MyClass.class.getModule().addReads(ClassLoader.getSystemClassloader().getUnnamedModule());
}

Dependency Injection

Modules can also be used for dependency injection. A module definition can specify any class as an implementation for an interface. Any other module can then request an implementation, without these modules being in any relationship. For example:

module printer.interface {
 exports printer;
}
package printer;
 public interface Printer {
 public abstract void print(String msg);
}
module printer.provider {
 requires printer.interface;
 provides printer.Printer with printer.impl.MyPrinter;
}
package printer.impl;
 public interface MyPrinter {
 public abstract void print(String msg);
}
module printer.user {
 requires printer.interface;
 uses printer.Printer;
}
ServiceLoader.load(printer.Printer.class).stream().forEach(
 l → l.get().print(“Hello world”)
);

Conclusion

There has been a lot of discussion about the module system, but after its final redesign, there is no reason not to run your application in Java 9. Granted, there are more powerful module / dependency injection systems out there, but these systems are also way more complicated and invasive in their nature. So, I’m sure this will make a fine addition to the Java world.