The main innovation in Java 9 was the introduction of modules. There was a lot of talk about this feature, the release date was postponed several times to finish everything properly. Today we’ll talk about the mechanism of modules, and what benefits Java 9 brought in general. The post is based on the report of Sergei Malkevich, IntexSoft Java developer.
To implement the modules in this version of Java, a whole new project was allocated — Project Jigsaw — which includes several JEPs and JSRs.
For those who like the official documentation, here you can learn more about each JEP.
Project Jigsaw started back in 2005: first, JSR 277 was released, and then in 2008 the actual work on the project began. It was released only in 2017. So it took almost 10 years to finish the Java modules in a proper way. Which, in fact, emphasizes the full scale of work and changes that were made during the implementation of modules.
Project goals:
Prior to version 9, JDK and JRE were monolithic. Their size grew with each release. Java 8 already occupied hundreds of Mb, and the developers had to “carry all this stuff” each time in order to run Java applications. Only rt.jar alone takes about 60 Mb. Well, here we can also add a slow start and high memory consumption. So, Java 9 can help here.
JDK 9 introduced the module system, namely, the JDK was divided into 73 modules. And each new version brings us a higher amount of these modules. In the 11 version, this number is close to 100. This separation allows developers to create the Jlink tool to create custom JRE that will include only those modules the application really needs. Thus, a simple application and some custom JRE with a minimal (or a small) set of modules can eventually fit in 20 Mb, which is good news.
You can check the list of modules here.
With Java 9, the JDK structure has changed: now it is identical to the JRE structure. If earlier the JDK included the JRE folder with bin and duplicate files, now everything looks as follows:
What is a module, actually? A module is a new level of packages and resources aggregation, or as developers say:
...a uniquely named, reusable group of related packages, as well as resources and a module descriptor.
Modules are delivered in JAR files with packages and a module descriptor — module-info.java. The module-info.java file contains: name, dependencies, public packages, consumed and offered services, reflection permissions.
Examples of module descriptor:
module java.sql {
requires transitive java.logging;
requires transitive java.transaction.xa;
requires transitive java.xml;
exports java.sql;
exports javax.sql;
uses java.sql.Driver;
}
module jdk.javadoc {
requires java.xml;
requires transitive java.compiler;
requires transitive jdk.compiler;
exports jdk.javadoc.doclet;
provides java.util.spi.ToolProvider with
jdk.javadoc.internal.tool.JavadocToolProvider;
provides javax.tools.DocumentationTool with
jdk.javadoc.internal.api.JavadocTool;
provides javax.tools.Tool with
jdk.javadoc.internal.api.JavadocTool;
}
First is the
module
keyword, followed by the name of the jdk.javadoc
package, which depends on another java.xml
package and is transitively dependent on other packages.Let’s take a closer look at the keywords:
requires
specifies the modules the current module depends on;requires transitive
specifies the transitive dependency: if the module m1 is transitively dependent on the module m2, and we have some third module mX, which depends on m1, then the module mX will also have access to m2;requires static
specifies the compile-time-only dependencies;exports
specifies the packages of the module that should be accessible for other modules (not including “sub-packages”);exports…to…
allows us to restrict the access: export
com.my.package.name
to
com.specific.package
; that is, we can open access to the package of our module only for some other(s) package(s) of another module;uses
specifies the services used by the module:uses java.sql.Driver;
In this case, we specify the interface of the service used.
provides
specifies the services provided by the module:provides javax.tools.Tool with
jdk.javadoc.internal.api.JavadocTool;
First we put the interface —
javax.tools.Tool
, and after with
— the implementation.Let’s say we have several modules connected which implement an abstract service —
MyService
. When assembling the application, we can decide what service implementation to use by dropping the desired service implementation modules to --module-path
:Iterable <MyService> services =
ServiceLoader.load(MyService.class);
Thus, the returned Iterator contains a list of implementations of the
MyService
interface. In fact, it will contain all the implementations found in the modules on --module-path
.Why services were introduced at all? They are needed to show how our code will be used. So it’s all about a semantic role. Also, modularity is about encapsulation and security, as we can make the implementation
private
and exclude the possibility of unauthorized reflection access.One more option of using services is a rather simple implementation of plugins. We can implement the plugin interface for our application and connect modules to work with them.
Let’s go back to declarations
Before Java 9 it was possible to use reflection to access almost everything, and we could do whatever we want. But as already mentioned, version 9 allows us to protect our app from “illegal” reflection access.
We can allow full reflection access to the module by declaring
open
:open module my.module { }
Or, we can expose specific packages with
opens
:module my.module { opens com.my.coolpackage;
}
It is also possible to use
opens…to
, thus opening the specific packages to the specific module(s).Project Jigsaw classifies modules as follows:
--list-modules
command.--module-path
and Java automatically creates a module with the name inherited from the name of the JAR file.--class-path
. This is a catch-all module for backward compatibility with previously written Java code.With modules, a new concept appeared — module-path. In fact, this is the classpath we all know, but for modules.
Starting a modular application looks like this:
In a “classic mode”, we specify options and the full path to the main class. If we want to work with modules, we also specify
-m
or -module
parameter, which indicates that we will run modules. That is, we automatically put our application in a “modular mode”. Then, we specify the module name and the path to the main class from the module.Also, in addition to classic
-cp
and --class-path
parameters we are used to working with, we add new -p
and --module-path
parameters, which indicate the paths to the modules used in the app.It often happens that developers do not switch to Java 9+ because they believe they will have to work with modules. Although in fact, we can run our applications without adding a parameter for modules and use only other new features of Java 9.
What is Jar Hell in a nutshell?
For example, our application depends on Library X and Library Y. Moreover, both of these libraries depend on Library Z, but on different versions: X depends on version 1, and Y depends on version 2.
It’s okay if version 2 is backward compatible with version 1, we’ll have no problem then. But if not, obviously, we’ll have a versions conflict, which means the same library cannot be loaded into memory by the same class loader.
How do developers get over such situations? There are standard methods that developers have been using since the very first Java, for example: exclude, or plugins for Maven, which rename the root packages of the library. Or sometimes developers are looking for different versions of Library X to find a compatible option.
In fact, the very first Jigsaw prototypes supposed the module might have a version and allowed different versions to be downloaded by different ClassLoaders. Later that idea was removed. As a result, the “silver bullet” many of us were waiting for did not work out.
But still, the developers secured us from such problems right out of the box. In Java 9, Split Packages are forbidden. These are the packages divided into several modules. That is, if we have the
com.my.coolpackage
in one module, we cannot use it in another module within the same app. If we run the application with modules containing the same packages, we’ll simply crash. This small improvement eliminates the possibility of unpredictable behavior connected with Split Packages downloading.
In addition to the modules themselves, there is a Jigsaw Layers mechanism, which also helps to cope with the Jar Hell.
A Jigsaw layer can be defined as some local module system. Here it is worth noting that the Split packages mentioned above are prohibited only within one Jigsaw layer. Modules with the same packages have a place to be, but they must belong to different layers.
It looks like this:
When the application starts, a Boot layer is created, which includes platform modules loaded by Bootstrap, additional platform modules loaded by the Extension loader, and modules of our application loaded by the Application loader.
We can create our own layers anytime and “put” modules of different versions there without any problems.
Here is a detailed presentation by Nikita Lipsky to learn more about the subject: Escaping The Jar Hell With Jigsaw Layers
The modules from Java 9 open up new possibilities for us, while the support for libraries today is quite limited. Of course, people run Spring, Spring Boot, and so on. But most libraries have not switched to the full use of modules. Apparently, that’s why all these changes were met rather skeptically by the tech community. Modules provide us with new opportunities, but the demand issue remains open.
And finally, here is a list of useful content to learn:
Previously published at https://intexsoft.com/