Introduction to the java platform module system (JPMS)

With version 9 of Java, a new module system is introduced into the language. This module system is used internally for the JDK libraries but can be used by the application code as well. In contrast to other module systems like OSGi, the Java Platform Module System (JPMS) is an integral part of the language. Because of this, I hope there will be many modularized java applications in the future.

Why should you modularize your code? This question would be a topic for a blog post in itself, but the main two reasons that stuck with me are: Organize your code in reusable units and manage dependencies. Most of the time we reuse a set of classes together instead of a single one. With modules, we have a convenient way to achieve this. Dependencies in Java are buried inside import definitions. With modules, they are explicitly stated and can be reasoned about without the help of code analysis tools.

Sample code for this post can be found on GitHub.

Module Descriptor

Let’s start with the definition of a module and its dependencies. A module is defined through the module descriptor file called module-info.java. This file must be located at the root of the source folder of the module.

The following snippet shows an example of such a module descriptor.

The descriptor defines a module named opus.address. The name must be unique and follow the same conventions as java package names. The first line inside the curly brackets states that the module exports the package com.opus.adress for use by other modules. Everything that is not explicitly exported by a module cannot be used by other modules of the application.

Dependencies

A module defines its dependencies through the requires keyword followed by the name of the module. When a module requires another one it “reads” this module. Reading a module allows the dependent module to use all public elements of packages that are exported by the read module. Java visibility rules still apply to the exported classes and interfaces.

Every module requires java.base implicitly. When a module uses other modules from the java library, it needs to read them explicitly as java.xml in the above example.

Readability is not transitive by default. Modules requiring opus.address do not automatically read opus.utils. If this is desired you need to add the transitive keyword as follows:

Every module that is requiring opus.address automatically reads opus.utils now. This is called implied readability and is transitive by itself. If opus.utils would have a transitive dependency on a third module, this would too be automatically read by modules requiring opus.address.

Services

Services are an integral part of the modules system. They provide means to decouple API and implementation and are in this sense a kind of dependency injection (DI) or inversion of control (IoC) mechanism. They can even be used in conjunction with common DI frameworks like spring or guice which is greatly explained here.

Providing a service

The following module descriptor shows the necessary parts to provide a service.

AddressRepository is the interface (API) of the service and AddressRepositoryImpl (guess what) the implementation of it. Note that the module does not export the package that contains the implementation. Therefore, the consumers of the service do know nothing about it.

Another module could too provide an AddressRepository with a CSV data source like this:

Consuming a service

Consuming a service is done through a lookup in the service registry. Some of you might know the service registry from previous versions of the JDK where it was used to provide services via metadata. In Java 9 the service registry is repurposed for use with the module system.

When a module provides a service, the module system registers it automatically within the service registry. Our code queries the registry for implementations of the service interface.

When you iterate over the returned iterator, the services are lazily instantiated for you by the service loader.

To instruct the module system to make a service available within our module, we need to declare a uses dependency on it in the module descriptor.

The uses dependencies are only resolved at runtime and don’t lead to an error when there are no repository implementations available. Our code needs to handle the situation when no implementation is available.

Wrap Up / Final Thoughts

I hope this post gave you a basic understanding of how to define modules in Java 9 and how they can help you write modularized code. If you are interested in topics like JPMS, modularity in general or domain driven design (DDD) make sure you subscribe to the opus blog newsletter. I will post a new article about every second Friday. Next up: a series about modularity patterns with samples using the JPMS.

Leave a Reply

Your email address will not be published. Required fields are marked *