Let's start with Java SE 9, the revolutionary element entailing the modularization of the JDK reached through the Jigsaw project (http://openjdk.java.net/projects/jigsaw/).
The main goals of this new feature are as follows:
- Make Java SE more flexible and scalable
- Improve security and maintainability
- Make it easier to construct, maintain, deploy, and upgrade large applications
- Enable improved performance
Two of the main criticisms made by the community toward the Java SE platform were the slowness in the release of the newer versions, as described previously, and the size of the JDK both in terms of space occupation and class size (approximately 4,240 classes in JDK 8).
In cloud environments and devices related to Internet of Things (IoT) architectures, these aspects represented an important limit.
But, it was not easy to find a solution that would allow overcoming these limits without abandoning one of the cornerstones of Java SE—the backward compatibility between versions.
It was, therefore, difficult to eliminate obsolete classes or remove classes designed for internal use but used intensively, especially by the framework, via Java reflection. The most famous example is the one put up by Sun Microsystems, which is related to the com.sun.misc.Unsafe class. It specified that: sun.* packages are not part of the supported, public Java interface. If you want to see the actual release, you can visit this link: https://www.oracle.com/technetwork/java/faq-sun-packages-142232.html
Through the Jigsaw project, it was not only possible to modularize the JDK in its core code but also to provide a tool for the realization of applications able to significantly decouple the interfaces of exposure of its services with respect to the actual implementation.
For JDK core code, it is possible to encapsulate JDK APIs, as described in JEP 260 (http://openjdk.java.net/jeps/260), using the following approach:
- Encapsulate, by default, all internal APIs that are considered non-critical.
- Encapsulate all internal APIs, that are considered critical, for which exist, in JDK 8, supported replacements.
- Do not encapsulate critical internal APIs, but implement the following steps:
- Deprecate them in JDK 9
- Define a plan to remove these APIs in JDK 10
- Implement a workaround solution via a command-line parameter
- Remove, from the JDK distribution, a limited number of supported Java Community Process (JCP) standard APIs
- Remove the extension mechanisms and the endorsed standards override
- Java EE modules, due to the Jigsaw project, are not resolved by default.
In this way, you can easily obtain a small bootable Java runtime that contains only the features, in terms of classes and interfaces that you really need, avoiding the presence of useless code and that can have only negative side effects in terms of footprint and space allocation.
You could easily analyze this using the following command:
$ java -listmods
As mentioned before, both the JDK and the application can benefit from modular development. Decoupling the components present in the applications is essential in microservice architectures, which need a very agile software life cycle to reduce time to market.
Using a modularity approach, you could easily achieve the following:
- Loose coupling between components
- Clear contracts and dependencies between components
- Hidden implementation using strong encapsulation
The main element in your implementation is the module.
Developers can organize their code into a modular structure, within which are declared dependencies inside their respective module definition files.
The properties of a module are defined into a file named module-info.java that contains the following attributes:
- The module name
- The module's packages that you want to make available publicly
- The dependencies, direct or transitive, that the module depends on
- The list of the services that the module consumes
- All possible implementation of the service that the module provides
The following are the main keywords used to set the main features of a module through the module-info.java file:
- module: The module definition file starts with this keyword followed by its name and definition.
- provides ... with ...: The provides keyword is used to indicate that the module provides implementations for a defined service interface. The service interface is expressed using the with keyword.
- requires: This keyword is used to indicate the dependencies of the modules. A module name has to be specified after this keyword and the list of dependencies are set through multiple required directives.
- transitive: This keyword is set after the requires keyword; with this feature, you are declaring that any module that depends on the module defining requires transitive <modulename> gets an implicit dependence on the <modulename>.
- uses: This keyword is used to indicate the service interface that this module is using; a type name, complete with fully qualified class or interface name, has to be specified after this keyword.
- opens: This keyword is used to indicate the packages that are accessible only at runtime; you can also use them for introspection, using Reflection APIs. This is quite important for libraries and frameworks that use reflection APIs in order to be as abstract as possible; the opens directive can also be set at module level—in this case, all packages of the module are accessible at runtime.
- exports: This keyword is used to indicate the packages of the module that are publicly available; a package name has to be specified after this keyword.
But the two approaches, Java Jigsaw module and Open Service Gateway Initiative (OSGi), have some differences.
OSGi's adoption is largely due to its support for dynamic component control. In this case, plugins or components are loaded dynamically and then activated, deactivated, and even updated or removed as needed. Presently, this dynamic module life cycle is not available with Java modules.
Additionally, compared with Java modules, OSGi supports improved versioning. Other OSGi advantages are related to isolation; for example, bundle changes require only the direct dependencies to be recompiled, whereas a Java module's entire layer, along with all child layers, need to be recompiled if just one module changes.
The downside is that OSGi bundles still suffer from class path issues, such as runtime exceptions for missing dependencies, or arbitrary class loading for packages with the same name.
Additionally, OSGi requires a class loader per module, which can affect some libraries that expect only a single class loader. Java modules don't allow split packages which is considered a big improvement in Java overall, and don't have similar class loader requirements or restrictions. One big advantage Java modules have over OSGi is compiler support.
I think we can get the most out of the modularization of the application components in a microservice architecture, combining the best of both technologies. The overall strategy is to use Java modules to modularize libraries (either imported or exported) and the JVM itself, and use OSGi on top to handle application modularity and dynamic life cycle control.