JB Header

Java 9 Modules Tutorial | Java Platform Module System (JPMS) with examples

Introduction This tutorial explains the Java Platform Module System (JPMS), commonly referred to as Java 9 Modules, which was the headline feature of Java 9Read about Java 9's release and Project Jigsaw. We will start by looking at the problems with the classpath which modules were designed to solve. We will then understand what a module is, go through the directives of the module-info.java module descriptor, and build a working two-module application which uses services. Lastly, we will see how strong encapsulation works in practice, how the JDK itself has been modularized, and how existing non-modular code fits into the module system. Problems with the classpath before Java 9 Before Java 9, a Java application was simply a set of JAR files placed on the classpath. The JVM saw all the classes from all these JARs as one flat list, which caused the following problems -
  1. No strong encapsulation: A public class was accessible to every other class on the classpath. Library authors had no way to mark packages as internal. A well-known example is sun.misc.Unsafe, an internal JDK class which many libraries came to depend upon.
  2. No reliable configuration ("JAR hell"): The classpath has no notion of which JAR depends on which. A missing dependency shows up only at runtime as a NoClassDefFoundError, and if two JARs contain the same class, whichever comes first on the classpath silently wins.
  3. A monolithic JDK: The Java runtime was shipped as one large rt.jar. Even a small application needed the entire runtime, which was a problem for small devices and containers.
Project Jigsaw was started to solve these problems, and it resulted in the Java Platform Module System. What is a module A module is a named, self-describing group of related packages, along with resources such as images and configuration files. Every module declares, in a file named module-info.java placed at the root of its source code -
  • its name,
  • the other modules it requires, i.e. its dependencies, and
  • the packages it exports, i.e. its public API. All packages which are not exported are hidden from other modules, even their public classes.
With this information, the module system can check at both compile time and startup that every required module is present, that there are no cycles or duplicate packages, and that no module accesses another module's internals.
The module descriptor - module-info.java A module declaration starts with the keyword module followed by the module's name. Module names follow the reverse-domain naming convention of packages, such as com.javabrahman.greetings. The body of the declaration contains zero or more of the following directives -
  • requires <module> - declares that this module depends on the given module. Every module implicitly requires java.base, which contains core packages such as java.lang and java.util.
  • requires transitive <module> - also makes the required module readable to any module which requires this module. It is used when the required module's types appear in this module's own API.
  • requires static <module> - declares a dependency which is mandatory at compile time but optional at runtime.
  • exports <package> - makes the public types of the given package accessible to all other modules. exports <package> to <module> limits this access to the named modules only.
  • opens <package> - allows other modules to access the package's types, including their private members, using reflection at runtime. Frameworks such as Spring and Hibernate need this. An entire module can be declared as open module to open all its packages.
  • provides <service-interface> with <implementation-class> - declares that this module provides an implementation of a service.
  • uses <service-interface> - declares that this module consumes a service, whose implementations it finds at runtime using java.util.ServiceLoader.
Note - module, requires, exports etc. are restricted keywords. They are keywords only inside module-info.java, so existing code which uses them as identifiers continues to compile. Example: Creating a two-module application Let us now build a small application with two modules -
  • com.javabrahman.greetings - a library module with a Greeter service interface and a GreetingFormatter utility class in its exported api package. Its internal package contains EnglishGreeter, an implementation of Greeter, which is not exported.
  • com.javabrahman.app - an application module which uses the greetings module.
By convention, the source code of each module is kept in a directory with the same name as the module, as shown below -
src
├── com.javabrahman.greetings
│   ├── module-info.java
│   └── com/javabrahman/greetings
│       ├── api
│       │   ├── Greeter.java
│       │   └── GreetingFormatter.java
│       └── internal
│           └── EnglishGreeter.java
└── com.javabrahman.app
    ├── module-info.java
    └── com/javabrahman/app
        └── GreetingApp.java
Module com.javabrahman.greetings
//module-info.java
module com.javabrahman.greetings {
 exports com.javabrahman.greetings.api;
 provides com.javabrahman.greetings.api.Greeter
     with com.javabrahman.greetings.internal.EnglishGreeter;
}

//Greeter.java
package com.javabrahman.greetings.api;
public interface Greeter {
 String greet(String name);
}

//GreetingFormatter.java
package com.javabrahman.greetings.api;
public class GreetingFormatter {
 public static String decorate(String greeting) {
  return "*** " + greeting + " ***";
 }
}

//EnglishGreeter.java
package com.javabrahman.greetings.internal;
import com.javabrahman.greetings.api.Greeter;
public class EnglishGreeter implements Greeter {
 @Override
 public String greet(String name) {
  return "Hello, " + name + "!";
 }
}
Module com.javabrahman.app
//module-info.java
module com.javabrahman.app {
 requires com.javabrahman.greetings;
 uses com.javabrahman.greetings.api.Greeter;
}

//GreetingApp.java
package com.javabrahman.app;
import java.util.ServiceLoader;
import com.javabrahman.greetings.api.Greeter;
import com.javabrahman.greetings.api.GreetingFormatter;
public class GreetingApp {
 public static void main(String args[]) {
  //Find the Greeter implementation(s) provided by other modules
  ServiceLoader<Greeter> greeterLoader = ServiceLoader.load(Greeter.class);
  for (Greeter greeter : greeterLoader) {
   String greeting = greeter.greet("JavaBrahman");
   System.out.println(GreetingFormatter.decorate(greeting));
   System.out.println("Implementation class -> " + greeter.getClass().getName());
  }
  //Which module is this class in?
  System.out.println("GreetingApp is in module -> " + GreetingApp.class.getModule().getName());
  System.out.println("Greeter is in module -> " + Greeter.class.getModule().getName());
  System.out.println("String is in module -> " + String.class.getModule().getName());
 }
}
Compiling and running the modules Both modules are compiled together using the --module-source-path option of javac, which tells it where to find the source of each module. The application is then run by placing the compiled modules on the module path using --module-path, and naming the module and main class to run using -m <module>/<main-class> -
javac -d out --module-source-path src $(find src -name "*.java")
java --module-path out -m com.javabrahman.app/com.javabrahman.app.GreetingApp
 OUTPUT of the above code
*** Hello, JavaBrahman! ***
Implementation class -> com.javabrahman.greetings.internal.EnglishGreeter
GreetingApp is in module -> com.javabrahman.app
Greeter is in module -> com.javabrahman.greetings
String is in module -> java.base
Explanation of the code
  • The greetings module exports only the com.javabrahman.greetings.api package. Its internal package stays hidden, even though EnglishGreeter is a public class.
  • Using provides Greeter with EnglishGreeter, the greetings module registers EnglishGreeter as an implementation of the Greeter service.
  • The app module requires the greetings module, which makes the exported api package available to it. It also declares that it uses the Greeter service.
  • In GreetingApp, ServiceLoader.load(Greeter.class) finds all implementations of Greeter provided by modules on the module path. It returns an instance of EnglishGreeter - a class which the app module cannot even refer to by name. The app depends only on the interface, while the implementation stays fully encapsulated.
  • Class.getModule(), a method added in Java 9, shows that each class now belongs to a module. Note that String belongs to java.base, which our app module never had to declare that it requires.
Strong encapsulation in action Let us now see what happens if the app module tries to use the EnglishGreeter class directly. We add the following class to the app module -
Accessing a class from a non-exported package
//Peek.java
package com.javabrahman.app;
import com.javabrahman.greetings.internal.EnglishGreeter;
public class Peek {
 public static void main(String args[]) {
  System.out.println(new EnglishGreeter().greet("Peek"));
 }
}
 OUTPUT on compiling the above code
Peek.java:2: error: package com.javabrahman.greetings.internal is not visible
import com.javabrahman.greetings.internal.EnglishGreeter;
                                ^
  (package com.javabrahman.greetings.internal is declared in module com.javabrahman.greetings, which does not export it)
1 error
Before Java 9, this code would have compiled without any issues, since EnglishGreeter is public. With modules, public no longer means accessible to everyone. A public type is accessible outside its module only if its package is exported. The compiler enforces this, and the JVM enforces it at runtime as well, including for reflection. (As a temporary migration aid, Java 9 to Java 15 still allowed classpath code to reflectively access JDK internals, printing an "illegal reflective access" warning. Java 16 and later deny such access by default.) Inspecting modules from the command line The java launcher can describe any module, which is a handy way to see what a module exports and requires. Describing our greetings module gives -
java --module-path out --describe-module com.javabrahman.greetings

com.javabrahman.greetings file:///<project-dir>/out/com.javabrahman.greetings/
exports com.javabrahman.greetings.api
requires java.base mandated
provides com.javabrahman.greetings.api.Greeter with com.javabrahman.greetings.internal.EnglishGreeter
contains com.javabrahman.greetings.internal
The output shows the exported package, the implicit (mandated) dependency on java.base, the service provided, and the internal package which the module contains but does not export. The modular JDK The JDK itself was split into modules in Java 9, and rt.jar was removed. Modules named java.* make up the Java SE platform, while jdk.* modules are specific to the JDK. Running java --list-modules lists all the modules of your JDK. Below is the description of the java.sql module, produced with a newer JDK (the version after the @ will match your JDK) -
java --describe-module java.sql

java.sql@24.0.1
exports java.sql
exports javax.sql
requires java.xml transitive
requires java.logging transitive
requires java.transaction.xa transitive
requires java.base mandated
uses java.sql.Driver
  • java.sql uses requires transitive for java.logging because types from java.util.logging appear in its API. Hence any module which requires java.sql can use those types without requiring java.logging itself.
  • uses java.sql.Driver shows that JDBC drivers are discovered as services, in exactly the same way our app discovered Greeter implementations.
  • Since the JDK is modular, the jlink tool introduced in Java 9 can create a custom runtime image containing only the modules an application needs, which is much smaller than the full JDK.
Migration: How non-modular code fits in Modules were designed so that existing applications continue to work, and can be migrated gradually -
  • The unnamed module: All JARs and classes on the classpath are placed in a single unnamed module. It can read all other modules and accesses all exported packages, so classpath-based applications run on Java 9 and later largely unchanged. Named modules, however, cannot require the unnamed module.
  • Automatic modules: A plain, non-modular JAR placed on the module path becomes an automatic module. Its name is taken from the Automatic-Module-Name entry in its manifest, or otherwise derived from the JAR file's name. An automatic module exports all its packages and reads all other modules, which lets modular code depend on libraries which have not been modularized yet.
  • Command line escape hatches: Code which depends on internals of other modules can be given access using options such as --add-exports and --add-opens while it is being migrated.
Note - a good way to start migrating a library is to add an Automatic-Module-Name entry to its JAR's manifest. This reserves a stable module name for the library, which its users can start requiring, before the library itself gets a module-info.java. Summary In this tutorial we saw the problems with the classpath which led to the Java Platform Module System, and what a module is. We went through the directives of the module-info.java module descriptor - requires, exports, opens, provides and uses. We then built, compiled and ran a two-module application which used a service, and saw how strong encapsulation prevents access to non-exported packages. Lastly, we looked at the modular JDK, and at how unnamed and automatic modules let existing code migrate to modules gradually.

Java 9 Modules Tutorial | Java Platform Module System (JPMS) with examples - JavaBrahman