Introduction to Modularity in Java 9


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.

In the root of our module we create a module-info.java :

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:

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.

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.

The nl.craftsmen.burgers.consumer module is created by running:

Now when the main method is executed via

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:

when trying to compile the naughty customer’s module via:

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:

Notice that the main class is specified so the module can be run accordingly:

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:

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:

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

Compile the modules and run the example as before:

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.

The nl.craftsmen.burgers.macdonalds module will implement the Restaurant interface:

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.

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.

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:

now compile all the modules

when running the application via

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:

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:

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.

Resources

[http://openjdk.java.net/projects/jigsaw/]

Een reactie plaatsen

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *