Bits of Java – Episode 17: Modules in Java
This week we will briefly discuss one, relative new, feature of the Java language, which is the Java Platform Module System (JPMS).
The JPMS was introduced in Java 9, with the purpose of introducing a new encapsulation level and allowing developers to build their applications in a more modular way. Does this definition sound complicated? Let’s see what we mean with encapsulation and modular.
You are probably all familiar with the Java access modifiers, right? These are special keywords you can add, or sometimes are automatically implied, in front of a class, an interface, a method and a field. They are:
private
- package-private (there is no keyword for that, it is the default one in some cases, when you do not specify anything else)
protected
public
For, instance, when applied to a class, they set the degree of accessibility to that class from another class:
private
means no other class can access that class;- package-private means only classes within the same package can access that class;
protected
means only classes within the same package or sub classes can access that class;public
means every other class can access that class.
Now, suppose you would like a class to be available only for certain other packages. How could you do that? Well, with only these access modifiers it is not possible. But, thanks to the JPMS now it is! The thing is that the JPMS introduced a new level, the module, which is simply a collection of packages, plus a file module-info.java
, in which you can specify which packages you want to make available from the outside and which package you wish to keep private. In addition, you can make a package available only to some specific other package, and, of course, you need to specify also which other modules yours depends on.
How does this help in building modular applications? First we need to define what we mean with the term modular application. In simple terms, a modular application can be described as an application which is made of many little pieces, each responsible for a certain functionality. In this sense, the term modular is often coupled with the concept of separation of concerns and micro-services. These can be seen as the tiny pieces of code which provides the different functionalities. Ideally, in a modular application, services are described by interfaces, which are accessible from the outside, and which defines a sort of contract, by specifying what that service is meant to provide. On the other hand, you have the implementation (or multiple implementations) of such service, which do not need to be seen by the outside and can be described as the set of operations required to achieve the functionality.
With the introduction of the JPMS, you can easily specify your service in a module, together with its implementation, and then make the interface available from other modules, while keeping the implementation details private.
This is just one, probably the most important, advantage of the new JPMS. But there are others. For instance, by specifying the modules on which your module depends, at startup Java does not need anymore to launch everything, but just the required things, reducing the startup time. For the same reason, when you export your product, you do not need to package the whole JDK, which results in much lighter products.
We will not enter here the details on how to migrate an existing standard application to a modular one, but I will briefly go through the main directives you can use on the module-info.java
file and their meaning. For that we will need some example classes, so let’s start defining them.
package my.service.contract
//This defines my service. It promises that calling getMessage
//we will get a String. This is what the contract says and what a
//consumer of this service should expect.
public interface MyService {
public String getMessage();
}
package my.service.impl
import my.service.contract.MyService;
//This is the implementation of the service, the piece of code
//which does the actual work. This contains the implementation
//details which are not needed to be visible from the outside
public class MyServiceImpl implements MyService {
public String getMessage() {
return "Hi Ilenia!";
}
}
package my.service.consumer
import my.service.contract.MyService;
import java.util.*;
//This class looks for the service through the ServiceLoader
//and then consumes the service, by calling the getMessage()
//method
public class MyServiceConsumer {
public static void main(String[] args) {
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for(MyService service : loader) {
System.out.println(service.getMessage());
}
}
}
So, we have created our service interface, MyService
which specifies the contract for the outside world; then we have an implementation of the service, MyServiceImpl
, which contains the details on how we provide the promised service, and we have a consumer of the service MyServiceConsumer
, which first looks for the service and then uses it.
These three ingredients are all in a different package. So, how to we connect them together? With their module-info.java
file! We will create three modules, one for each package. Let’s start with the module for the service interface.
module my.service.contract {
exports my.service.contract;
}
So, what we did was to specify that we are defining a module, using the keyword module
followed by the module name. Then we specify the directives for such module. In this case, since we need the service interface to be available to both the other modules, we export it, using the exports
keyword, followed by the package name.
The module-info.java
of our service implementation will look like this:
module my.service.impl {
requires my.service.contract;
provides my.service.MyService with my.service.MyServiceImpl;
}
Here we have two new directives. The first one specifies that we need the module my.service.contract
for our module to compile. Indeed in MyServiceImpl
we need to import MyService
. This is done with the directive requires
, followed by the name of the module that we need to import.
The second directive, instead, specifies that we are providing an implementation for a certain service. The keyword is provides
, followed by the fully qualified name of the service interface, then with
, followed by the fully qualified name of the service implementation.
What about our consumer? Its module-info.java
will look like:
module my.service.consumer {
requires my.service.contract;
uses my.service.contract.MyService;
}
It also needs to require the module with the service interface, otherwise our MyServiceConsumer
class will not compile. Then, since we are looking for a service through the ServiceLoader
we need the uses
directive, followed by the fully qualified name of the service interface.
Notice here that we do not need to require the module with the service implementation. Indeed, we did not export the service implementation. This is very useful, especially because, if you later want to add a fourth module which provides an alternative implementation for MyService
, you won’t need to recompile any of these modules, and the ServiceLoader
will find the new implementation as well!
Keep in mind that this is not an exhaustive discussion about modules, and once you have your module-info.java
files you still need to compile and package your modules. But I still hope you learned something new and maybe start using modules for your application!
Next week we will talk about another interesting feature of Java, annotations. Stay tuned!
by Ilenia Salvadori