Java 9 SE is expected to be released somewhere in the beginning of the second half of 2017, and from all the changes in this release, the biggest one is JSR-376: Java Platform Module System. The project started in 2008 and was originally scheduled to be released in the Java 7 release, but has been deferred to java 8 and later to java 9.
So what are modules? Well a module is a collection of code like a Jar file except that a module has some additional metadata in the from of a module-info.java
file that specifies which other modules it depends on and which packages from its own module are accessible by other modules and only these packages are accessible by other modules.
On the left we have two modules. Each module consists of class files divided over one or more packages. For code in module B to be able to access code in module B we have to do two things: Module A must state that is requires module B and Module B must state that it exports one or more packages making them readable for module A. This metadata is stored in the
module-info.java
file that is included in the module as well.
By specifying which other modules are required by a module, it is possible to create a complete module graph (like a dependency tree) when you look deeper into the required modules of a module and so forth all the way down to the last base module. By specifying which packages inside a module are accessible by other modules, it is possible to hide certain functionality. As a result public no longer means that any class can access it; public classes or methods can only be accessed from within the module and can only be accessed by other modules if the package of the class or method is exported and therefore is accessible. This makes it possible to finally hide your implementation of a public interface.
This leads to new advantages:
- Reliable configuration: We know exactly which modules are required by a module.
- Strong encapsulation: The module itself specifies which packages are accessible and therefore any other packages in the module are not.
The new Module system is not only made out of modules; It also contains a module path.
The module path is where the compiler or the JVM will resolve modules. Because a module contains the information about which modules it requires the JVM can resolve classes in a more controlled way that is quicker than the classpath. The Classpath is a just a large collection of java libraries. When a class is loaded from the classpath the JVM will search in each jar on the Classpath for the required class. So if multiple versions of a class are on the classpath, it’s up to the classloader which version is resolved and if it is the wrong one you in in JAR Hell.
Not only does the new JVM know how to handle modules, the JVM itself has been completely modularized. The graph below holds a complete overview of the modules in JDK 9.
At the base you can see the java.base module which contains all primary classes needed for the java language such as java.lang.Object. All internal packages that started with sun.*
, or having the name internal
in the package were never intended to be directly used by external code and now finally reside in a module and are encapsulated. This means that this code is no longer accessible. If your code uses one of these classes directly it will not be able to run on Java 9. As an example one of the most widely used classes from the internal packages of java 7 and before, is the class sun.misc.Base64
, used for base64 encoding and decoding. That is why in java 8 the Base64 class was moved from the sun.misc
package to java.util
. An other feature of Java 8 was the jdeps tool so you could analyze the packages needed by your code.
Modules in practice
Let’s jump into the code by looking at some examples. The following examples can be downloaded from [https://gitlab.com/craftsmen/java9-modularity]().
To be able to compile the code and use modules you will need to install an early access java 9 JDK which can be downloaded from [https://jdk9.java.net/download/]().
Basic Modules
Let’s start with two simple modules:
- Module
nl.craftsmen.burgers.consumer
in need of a burger. - Module
nl.craftsmen.burgers.macdonalds
that can fulfill the consumer’s needs.
You can find the source of these modules in the burgers_modules/src folder of the checked out project. In the src
folder you will see the directories nl.craftsmen.burgers.consumer
and nl.craftsmen.burgers.macdonalds
. These directories contain the resources for similarly named modules.
The MacDonalds
class in the nl.craftsmen.burgers.macdonalds
module has a public method for serving burgers and an instance of the class Kitchen
which is also located in the same module but in a different package.
package nl.craftsmen.burgers.macdonalds;
import nl.craftsmen.burgers.macdonalds.internal.Kitchen;
public class MacDonalds {
private Kitchen kitchen = new Kitchen();
public String serveBurger(){
return kitchen.createBurger();
}
}
In the root of our module we create a module-info.java
:
module nl.craftsmen.burgers.macdonalds {
requires java.base;
exports nl.craftsmen.burgers.macdonalds;
}
We gave the module a name and told it which modules it requires, namely java.base
. The module java.base
is implicitly required so we could have left it out in our module-info.java
. The java.base
is the only exception because it’s required by any module. And last we specified that the package nl.craftsmen.burgers.macdonalds
is accessible by other modules. The module-info.java
is a java file and will be compiled by the compiler, but only by a java 9 compiler. As you can see class name is not valid so it is ignored by previous compilers.
Let’s create a module from our MacDonalds project by running the command from the commandline from the burger_modules
directory:
javac --module-path mods -d mods/nl.craftsmen.burgers.macdonalds src/nl.craftsmen.burgers.macdonalds/nl/craftsmen/burgers/macdonalds/internal/Kitchen.java src/nl.craftsmen.burgers.macdonalds/nl/craftsmen/burgers/macdonalds/MacDonalds.java src/nl.craftsmen.burgers.macdonalds/module-info.java
Here we told the compiler via ‘–module-path mods’ where our module path is so it can find modules that are required by our module and with -d mods/nl.craftsmen.burgers.macdonalds
we specify the target location for our module.
Now let’s have a look at the nl.craftsmen.burgers.consumer
module. It has a ‘Main’ class that has a main method that will invoke a method from the MacDonalds
class in the nl.craftsmen.burgers.macdonalds
module.
package nl.craftsmen.burgers.consumer;
import nl.craftsmen.burgers.macdonalds.MacDonalds;
public class Main {
public static void main(String[] args) {
MacDonalds md = new MacDonalds();
System.out.println("Consumer sais: Macdonalds served me a " + md.serveBurger());
}
}
and we need a module-info.java
of course for this module that requires the nl.craftsmen.burgers.macdonalds
module (and the java.base
module but here we left it out because the java.base
module is implicit). Nothing is exported from this module because we are going to call the main method directly from the command line.
module nl.craftsmen.burgers.consumer{
requires nl.craftsmen.burgers.macdonalds;
}
The nl.craftsmen.burgers.consumer
module is created by running:
javac --module-path mods -d mods/nl.craftsmen.burgers.consumer src/nl.craftsmen.burgers.consumer/nl/craftsmen/burgers/consumer/Main.java src/nl.craftsmen.burgers.consumer/module-info.java
Now when the main method is executed via
java --module-path mods -m nl.craftsmen.burgers.consumer/nl.craftsmen.burgers.consumer.Main
we see in the output that a Big Mac is served to the consumer.
But what will happen if we directly access a class from a module that is not exported by the module? To demonstrate what happen, let’s have a look at the Naugthy Consumer class:
package nl.craftsmen.burgers.naughtyconsumer;
import nl.craftsmen.burgers.macdonalds.internal.Kitchen;
public class Main {
public static void main(String[] args) {
Kitchen kitchen = new Kitchen(); // The Kitchen class is not exported
System.out.println("Consomer sais: Macdonalds served me a " + kitchen.createBurger());
}
}
module nl.craftsmen.burgers.naughtyconsumer{
requires java.base;
requires nl.craftsmen.burgers.macdonalds;
}
when trying to compile the naughty customer’s module via:
javac --module-path mods -d mods/nl.craftsmen.burgers.naughtyconsumer src/nl.craftsmen.burgers.naughtyconsumer/nl/craftsmen/burgers/naughtyconsumer/Main.java src/nl.craftsmen.burgers.naughtyconsumer/module-info.java
this wil result in a compilation error because the package where the Kitchen class resides is not exported. You can use reflection to outsmart the compiler, but you will be faced with a runtime exception when trying to access the kitchen.
packaging
When compiling your code like in the examples above with the -d option, a module folder is created in the mods
folder that wil hold all your compiled classes.
This folder can be packaged into a jar as follows:
jar --create --file=mods/nl.craftsmen.burgers.consumer.jar --main-class=nl.craftsmen.burgers.consumer.Main -C mods/nl.craftsmen.burgers.consumer
Notice that the main class is specified so the module can be run accordingly:
java --module-path mods -m nl.craftsmen.burgers.consumer
Implied readibility
Modularity has an option for exporting packages from a required module to another module.
From the previous example the Burger is upgraded from a simple String
Class to a model class called Burger. This Burger
class resides in its own module that exports this model. The source can be found in the burger_impliedreadibility
folder in the gitlab project.
The module-info.java
looks as follows:
module nl.craftsmen.burgers.model {
exports nl.craftsmen.burgers.model;
}
The MacDonalds
class in nl.craftsmen.burgers.macdonalds
module is updated as well in that the return type of the serverBurger
method is changed to the new model class:
public Burger serveBurger(){
return kitchen.createBigMac();
}
The model class from a different module is now returned to the nl.craftsmen.burgers.consumer
module. To make the nl.craftsmen.burgers.model
module readable for the nl.craftsmen.burgers.consumer
package the module-info.java
of the nl.craftsmen.burgers.macdonalds
module is updated in this way because model package is required and is at the same time being exposed. requires public
makes this possible
module nl.craftsmen.burgers.macdonalds {
requires public nl.craftsmen.burgers.model;
exports nl.craftsmen.burgers.macdonalds;
}
Compile the modules and run the example as before:
javac --module-path mods -d mods/nl.craftsmen.burgers.model src/nl.craftsmen.burgers.model/nl/craftsmen/burgers/model/Burger.java src/nl.craftsmen.burgers.model/module-info.java
javac --module-path mods -d mods/nl.craftsmen.burgers.macdonalds src/nl.craftsmen.burgers.macdonalds/nl/craftsmen/burgers/macdonalds/internal/Kitchen.java src/nl.craftsmen.burgers.macdonalds/nl/craftsmen/burgers/macdonalds/MacDonalds.java src/nl.craftsmen.burgers.macdonalds/module-info.java
javac --module-path mods -d mods/nl.craftsmen.burgers.consumer src/nl.craftsmen.burgers.consumer/nl/craftsmen/burgers/consumer/Main.java src/nl.craftsmen.burgers.consumer/module-info.java
java --module-path mods -m nl.craftsmen.burgers.consumer/nl.craftsmen.burgers.consumer.Main
Macdonalds gave me a BigMac!
Yes !!! Everything still works.
Services
As seen so far the module system and its dependency graph tend to be tightly coupled. This of course is not how we develop code nowadays. With services we create loosely coupled applications while still using the advantages of modules.
From the macdonalds
class in the nl.craftsmen.burgers.macdonalds
module we extract an interface and put that interface in a separate module nl.craftsmen.burgers.restaurant
and let that module export the interface. By now you should know how the module-info.java
should look like, so it is not mentioned here but you can find it in the burgers_services folder in the gitlab project.
package nl.craftsmen.burgers.restaurant.api;
public interface Restaurant{
public String serveBurger();
}
The nl.craftsmen.burgers.macdonalds
module will implement the Restaurant
interface:
package nl.craftsmen.burgers.macdonalds;
import nl.craftsmen.burgers.restaurant.api.Restaurant;
import nl.craftsmen.burgers.macdonalds.internal.Kitchen;
public class MacDonalds implements Restaurant{
private Kitchen kitchen = new Kitchen();
public String serveBurger(){
return kitchen.createBurger();
}
}
The module-info.java
of the nl.craftsmen.burgers.macdonalds
module will state that it provides an implementation for Restaurant
interface and the package of the implementation is of course exported.
module nl.craftsmen.burgers.macdonalds {
requires nl.craftsmen.burgers.restaurant;
provides nl.craftsmen.burgers.restaurant.api.Restaurant
with nl.craftsmen.burgers.macdonalds.MacDonalds;
}
From our nl.craftsmen.burgers.consumer
module we wil not refer to the nl.craftsmen.burgers.macdonalds
module directly but insteed we wil use a ServicLoader
in order to get references to any implementations of the Restaurant
interface.
package nl.craftsmen.burgers.consumer;
import java.util.ServiceLoader;
import nl.craftsmen.burgers.restaurant.api.Restaurant;
public class Main {
public static void main(String[] args) {
ServiceLoader serviceloader = ServiceLoader.load(Restaurant.class);
System.out.println("Available burgers to the consumer:");
for (Restaurant restaurant : serviceloader) {
System.out.println("- " + restaurant.serveBurger());
}
}
}
The nl.craftsmen.burgers.consumer
module does not require the nl.craftsmen.burgers.macdonals
module any more but only requires the nl.craftsmen.burgers.restaurant
for the Restaurant
interface. The module-info.java
will look like this:
module nl.craftsmen.burgers.consumer{
requires nl.craftsmen.burgers.restaurant;
uses nl.craftsmen.burgers.restaurant.api.Restaurant;
}
now compile all the modules
javac -d mods/nl.craftsmen.burgers.restaurant src/nl.craftsmen.burgers.restaurant/nl/craftsmen/burgers/restaurant/api/Restaurant.java src/nl.craftsmen.burgers.restaurant/module-info.java
javac --module-path mods -d mods/nl.craftsmen.burgers.consumer src/nl.craftsmen.burgers.consumer/nl/craftsmen/burgers/consumer/Main.java src/nl.craftsmen.burgers.consumer/module-info.java
javac --module-path mods -d mods/nl.craftsmen.burgers.macdonalds src/nl.craftsmen.burgers.macdonalds/nl/craftsmen/burgers/macdonalds/internal/Kitchen.java src/nl.craftsmen.burgers.macdonalds/nl/craftsmen/burgers/macdonalds/MacDonalds.java src/nl.craftsmen.burgers.macdonalds/module-info.java
when running the application via
java --module-path mods -m nl.craftsmen.burgers.consumer/nl.craftsmen.burgers.consumer.Main
you will see that the The Big Mac is available to the consumer.
After compiling another module called nl.craftsmen.burgers.burgerking
that implements and provides a service for the Restaurant
interface, as the nl.craftsmen.burgers.macdonalds
module did, and running the nl.craftsmen.burgers.consumer
module again, you wil see that the service of the nl.craftsmen.burgers.burgerking
module is found by the ServiceLoader.
The module nl.craftsmen.burgers.burgerking
is compiled by executing the command:
javac --module-path mods -d mods/nl.craftsmen.burgers.burgerking src/nl.craftsmen.burgers.burgerking/nl/craftsmen/burgers/burgerking/internal/Kitchen.java src/nl.craftsmen.burgers.burgerking/nl/craftsmen/burgers/burgerking/BurgerKing.java src/nl.craftsmen.burgers.burgerking/module-info.java
We only compiled the nl.craftsmen.burgers.burgerking
module and put it on the module path and it all works!!!
JLink
As stated before with modules we have reliable configuration thanks to all the module-info
‘s from which we can create a full dependency graph. It is even possible to see what classes of the JRE are required for a module and which are not required. This creates the option to create a runtime image with only the required modules and not a full JRE backing your application. The size of this image can be reduced a lot depending on how many modules are required. This becomes handy when you need to run your application on a device with limited resources. You create your image by using the JLink tool that is included in Java 9 jdk. To create an image from our nl.craftsmen.burger.consumer
module you run it like this:
jlink --module-path d:\Java\jdk9-ea-133\jmods;mods --add-modules nl.craftsmen.burgers.consumer --output image
This will create a folder named image
, that will hold a minimal runtime environment and all our modules. The total size of the image for our module is about 31MB. This is quite a large reduction since your full JRE will take up to around 200MB.