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
The module descriptor - module-info.java
A module declaration starts with the keyword
Compiling and running the modules
Both modules are compiled together using the
OUTPUT of the above code
Explanation of the code
OUTPUT on compiling the above code
Before Java 9, this code would have compiled without any issues, since
The output shows the exported package, the implicit (
Migration: How non-modular code fits in
Modules were designed so that existing applications continue to work, and can be migrated gradually -
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 -
- No strong encapsulation: A
publicclass was accessible to every other class on the classpath. Library authors had no way to mark packages as internal. A well-known example issun.misc.Unsafe, an internal JDK class which many libraries came to depend upon. - 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. - 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.
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.
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 requiresjava.base, which contains core packages such asjava.langandjava.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 asopen moduleto 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 usingjava.util.ServiceLoader.
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 aGreeterservice interface and aGreetingFormatterutility class in its exportedapipackage. Itsinternalpackage containsEnglishGreeter, an implementation ofGreeter, which is not exported.com.javabrahman.app- an application module which uses the greetings module.
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());
}
}
--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
*** 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
- The greetings module
exportsonly thecom.javabrahman.greetings.apipackage. Itsinternalpackage stays hidden, even thoughEnglishGreeteris apublicclass. - Using
provides Greeter with EnglishGreeter, the greetings module registersEnglishGreeteras an implementation of theGreeterservice. - The app module
requiresthe greetings module, which makes the exportedapipackage available to it. It also declares that itusestheGreeterservice. - In
GreetingApp,ServiceLoader.load(Greeter.class)finds all implementations ofGreeterprovided by modules on the module path. It returns an instance ofEnglishGreeter- 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 thatStringbelongs tojava.base, which our app module never had to declare that it requires.
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"));
}
}
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
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
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.sqlusesrequires transitiveforjava.loggingbecause types fromjava.util.loggingappear in its API. Hence any module which requiresjava.sqlcan use those types without requiringjava.loggingitself.uses java.sql.Drivershows that JDBC drivers are discovered as services, in exactly the same way our app discoveredGreeterimplementations.- Since the JDK is modular, the
jlinktool 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.
- 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-Nameentry 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-exportsand--add-openswhile it is being migrated.
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.Continue Reading Java 9 & Beyond Tutorials
Java 9 Immutable Collections using List.of, Set.of, Map.ofClick to read tutorial on Java 9 immutable collections Overview of New Features in Java 10Click to read overview of Java 10 features Java 9 released after several rounds of delaysClick to read about Java 9's release Applet being prepped for an exit in Java 9Click to read about Applet's deprecation in Java 9
Java 9 Immutable Collections using List.of, Set.of, Map.ofClick to read tutorial on Java 9 immutable collections Overview of New Features in Java 10Click to read overview of Java 10 features Java 9 released after several rounds of delaysClick to read about Java 9's release Applet being prepped for an exit in Java 9Click to read about Applet's deprecation in Java 9