Java Platform Module System (JPMS)

Naveen AnamaneniUpdated · 7 min read

A module is a named group of packages that states two things explicitly: which other modules it needs, and which of its packages other modules may use. Everything else inside it is hidden. The Java Platform Module System (JPMS), added in Java 9, brought modules to the language, and the JDK itself was split into about 70 of them. A module is described by one small file at its root, module-info.java. In this lesson, you will learn what problems modules solve, how to write a module descriptor, and how to compile and run a small application made of two modules.

What Was Wrong Before Modules

Problem with the class pathHow modules fix it
No real encapsulation. Any public class in any JAR can be used by anyone, including classes meant to be internalA module exports only chosen packages; the rest can't be reached, even if public
Missing JARs are found late. A forgotten dependency shows up as NoClassDefFoundError at run time, maybe days laterDependencies are declared; a missing module is reported at startup
"JAR hell". Two JARs containing the same package silently shadow each otherA package may belong to only one module; duplicates are an error
One huge JDK. Every program carried the whole runtimeA program can be shipped with only the modules it needs (see "Jlink in Java: Custom Runtime Images")

The Module Descriptor

module-info.java
module expenses.core {
    exports com.poolstring.expenses.model;
    exports com.poolstring.expenses.service;
}
PartMeaning
module expenses.coreDeclares a module and its name
exports <package>Other modules may use the public types of this package
requires <module>This module needs another module
(not mentioned)A package that isn't exported is internal to the module

The file is named module-info.java, sits in the root of the module's source folder, and compiles to module-info.class. Every module automatically requires java.base, which holds java.lang, java.util, java.io and other core packages.

Kinds of Module

KindWhat it is
Named (explicit) moduleHas a module-info, placed on the module path
Automatic moduleAn ordinary JAR placed on the module path; it gets a name from its file name and exports everything
Unnamed moduleAll code on the class path; can read every module
Platform modulesThe JDK's own modules: java.base, java.sql, java.xml, java.net.http and so on

Example: Looking at Modules from Code

Every class knows its module:

Modules.java▶ Try it
import java.lang.module.ModuleDescriptor;
import java.sql.Connection;
import java.util.List;
 
public class Modules {
    public static void main(String[] args) {
        Module base = String.class.getModule();
        System.out.println(String.class.getSimpleName() + " is in "
            + base.getName());
        System.out.println(Connection.class.getSimpleName() + " is in "
            + Connection.class.getModule().getName());
        System.out.println(List.class.getPackageName() + " exported? "
            + base.isExported("java.util"));
        System.out.println("jdk.internal.misc exported? "
            + base.isExported("jdk.internal.misc"));
 
        Module mine = Modules.class.getModule();
        System.out.println("This class: named module? " + mine.isNamed()
            + ", name: " + mine.getName());
 
        ModuleDescriptor sql = Connection.class.getModule().getDescriptor();
        System.out.println("java.sql requires:");
        sql.requires().stream()
            .map(r -> "  " + r.name()
                + (r.modifiers().contains(
                    ModuleDescriptor.Requires.Modifier.TRANSITIVE)
                    ? " (transitive)" : ""))
            .sorted()
            .forEach(System.out::println);
        System.out.println("java.sql exports:");
        sql.exports().stream().map(e -> "  " + e.source()).sorted()
            .forEach(System.out::println);
 
        long total = ModuleLayer.boot().modules().size();
        System.out.println("More than 20 modules loaded at startup? "
            + (total > 20));
    }
}
Output
String is in java.base
Connection is in java.sql
java.util exported? true
jdk.internal.misc exported? false
This class: named module? false, name: null
java.sql requires:
  java.base
  java.logging (transitive)
  java.transaction.xa (transitive)
  java.xml (transitive)
java.sql exports:
  java.sql
  javax.sql
More than 20 modules loaded at startup? true

Explanation:

  • String lives in java.base and Connection in java.sql.
  • java.util is exported by java.base; jdk.internal.misc isn't, so your code can't use it. That is the JDK protecting its internals.
  • This program runs from the class path, so its class is in the unnamed module: no name, and no module-info.
  • The descriptor of java.sql shows what it requires and exports. "Transitive" is explained in "Requires, Exports and Opens in Java Modules".

Example: A Two-Module Application

This chapter builds a small expense tracker from two modules:

Project layout
expenses/
  src/
    expenses.core/
      module-info.java
      com/poolstring/expenses/model/Expense.java
      com/poolstring/expenses/service/ExpenseTracker.java
      com/poolstring/expenses/internal/Rounding.java
    expenses.app/
      module-info.java
      com/poolstring/expenses/app/Main.java

expenses.core contains the logic and exports two of its three packages. expenses.app contains main and needs the core:

expenses.app/module-info.java
module expenses.app {
    requires expenses.core;
}
Main.java
package com.poolstring.expenses.app;
 
import com.poolstring.expenses.service.ExpenseTracker;
 
public class Main {
    public static void main(String[] args) {
        ExpenseTracker tracker = new ExpenseTracker();
        tracker.add("Food", "450.50");
        tracker.add("Travel", "1200");
        tracker.add("Food", "199.25");
 
        System.out.println("By category: " + tracker.byCategory());
        System.out.println("Total: " + tracker.total());
        System.out.println("Running in module: "
            + Main.class.getModule().getName());
    }
}

Compile both modules and run the application:

Terminal
javac -d out --module-source-path src $(find src -name "*.java")
java --module-path out -m expenses.app/com.poolstring.expenses.app.Main
Terminal output
By category: {Food=649.75, Travel=1200}
Total: 1849.75
Running in module: expenses.app

Explanation:

  1. --module-source-path src tells javac that each folder under src is a module. The output folder out gets one subfolder per module, each with a module-info.class.
  2. --module-path out tells java where to find modules (the module path, not the class path).
  3. -m module/mainclass names the module and the class to start.
  4. The JVM checks the module graph at startup: expenses.app requires expenses.core, which requires java.base. If any were missing, the program wouldn't start.

java --describe-module prints what a module declares:

java --module-path out --describe-module expenses.core
exports com.poolstring.expenses.model
exports com.poolstring.expenses.service
requires java.base mandated
contains com.poolstring.expenses.internal

The internal package is listed as contains: it is in the module but not exported.

Do You Have to Use Modules?

No. The class path still works, and most applications run from it, in the unnamed module. Modules pay off most for libraries that want to hide internals, and for applications that ship their own trimmed-down runtime. Even if you never write a module-info.java, the JDK's own modules affect you: its internal packages are closed, which is why very old libraries that reached into them needed updates.

Common Mistakes

Putting module-info.java in a package folder. It belongs in the root of the module's sources.

Expecting public to mean "usable everywhere". In a module, a public class in a non-exported package is hidden from other modules.

Mixing up the two paths. Modules go on --module-path; putting a modular JAR on the class path makes it behave like an ordinary JAR.

The same package in two modules. This "split package" is an error.

Interview Corner

What is the Java Platform Module System?

JPMS, introduced in Java 9, adds modules: named, self-describing collections of packages. A module's module-info.java declares which modules it requires and which packages it exports; non-exported packages are strongly encapsulated, even their public types. Benefits are reliable configuration (missing or conflicting modules are detected at startup), strong encapsulation of internals, and the ability to build small custom runtime images with jlink. The JDK itself is modularized, with java.base as the module every other module depends on.

What is the difference between a module and a package?

A package groups related classes and controls access through public, protected and package-private. A module groups related packages and adds a higher level of access control: it decides which packages are visible to other modules at all, and states its dependencies on other modules. A JAR on the class path has no such boundary.

Practice

  1. Easy: Run java --list-modules and count the modules whose names start with java..
  2. Harder: Create the two-module project above, compile it, and run it. Then run java --module-path out --describe-module expenses.app.

Quick check: Modules

4 questions
1In which file is a module declared?
2Which module does every module require automatically?
3A public class is in a package that its module doesn't export. Who can use it?
4In which Java version were modules introduced?

Key Takeaways

Was this tutorial helpful?

Lesson 514 of 578