Java Modules (JPMS) Basics

Read and write a module-info.java, understand strong encapsulation and the module path, and see where modules matter for a back-end service.

Syntaxmodule com.example.shop { requires java.net.http; exports com.example.shop.api; opens com.example.shop.dto; }

A module is a named set of packages with a descriptor, module-info.java, that states which other modules it needs and which of its packages other code may use. The Java Platform Module System arrived in Java 9, when the JDK itself was split into modules such as java.base, java.sql and java.net.http.

The descriptor

module com.example.shop {
    requires java.net.http;
    requires transitive com.example.money;
    requires static com.example.annotations;
    exports com.example.shop.api;
    opens com.example.shop.dto to com.fasterxml.jackson.databind;
}
  • requires: this module reads that one. Every module reads java.base without saying so.
  • requires transitive: modules that require this one read that one too. Use it when your public API exposes its types.
  • requires static: needed at compile time, optional at run time.
  • exports: public types in that package can be used by other modules.
  • opens: the package is open to deep reflection at run time, which libraries such as Jackson and Hibernate need to reach private fields and constructors.
  • uses and provides ... with: plug-ins found through ServiceLoader.

Strong encapsulation

In a named module, public means public inside the module until the package is exported. The JDK applies the same rule to its own internals: since Java 16 that is the default, and since Java 17 the old --illegal-access switch is gone. Code cannot reach classes such as jdk.internal.misc.Unsafe, or private fields of JDK classes through reflection, unless the command line grants it with --add-exports or --add-opens. When an old library throws InaccessibleObjectException after a JDK upgrade, this is why, and upgrading the library is the real fix.

Class path and module path

Everything on the class path lands in one unnamed module, which reads every other module and exports all its packages. That is how most services run, Spring Boot ones included, and why you can build back-ends for years without a module-info.java. JARs on the module path (--module-path) become named modules; a plain JAR there becomes an automatic module, named from its Automatic-Module-Name manifest entry or, failing that, from its file name. A named module cannot depend on code in the unnamed module.

Two rules catch people moving to modules: a package may belong to only one module, so a "split package" across two modules is rejected, and requires may not form a cycle.

Where modules pay off

Even if your service never gets a descriptor, the JDK's modules matter for deployment. jdeps --print-module-deps lists the JDK modules your JAR uses, and jlink builds a runtime containing only those, which shrinks container images. Libraries gain the most from a real descriptor: they can hide internal packages for real and change them freely between releases.

Example

Example · java
// src/main/java/module-info.java for a small orders service
module com.example.orders {
    // JDK modules this code uses (java.base is implicit)
    requires java.net.http;
    requires java.sql;
    requires jdk.httpserver;

    // A library module, read from the module path
    requires com.fasterxml.jackson.databind;

    // The public API other modules may compile against
    exports com.example.orders.api;

    // DTOs are bound by Jackson through reflection, so open them to it only
    opens com.example.orders.dto to com.fasterxml.jackson.databind;

    // com.example.orders.internal is neither exported nor opened:
    // its public classes are invisible outside this module.
}

When to use it

  • Open a DTO package to Jackson in module-info.java so JSON binding works without exporting it.
  • Diagnose an InaccessibleObjectException from an old library after moving a service to Java 25.
  • Use jdeps and jlink to build a trimmed runtime for a service's container image.

More examples

Reading a module descriptor at run time

java --describe-module java.sql prints the same information from the command line. The MANDATED requirement on java.base is the one every module gets without writing it.

Example · java
void main() {
    ModuleDescriptor sql = ModuleLayer.boot().findModule("java.sql").orElseThrow().getDescriptor();

    sql.requires().stream()
            .sorted(Comparator.comparing(ModuleDescriptor.Requires::name))
            .forEach(r -> IO.println("requires " + r.name() + " " + r.modifiers()));
    sql.exports().stream()
            .map(ModuleDescriptor.Exports::source)
            .sorted()
            .forEach(p -> IO.println("exports " + p));

    // Code started from the class path, or from a single source file, is in the unnamed module
    IO.println("named: " + getClass().getModule().isNamed());
}

// Output:
// requires java.base [MANDATED]
// requires java.logging [TRANSITIVE]
// requires java.transaction.xa [TRANSITIVE]
// requires java.xml [TRANSITIVE]
// exports java.sql
// exports javax.sql
// named: false

Strong encapsulation of JDK internals

A public class in a package that is not exported is out of reach, even through reflection. Adding --add-exports java.base/jdk.internal.misc=ALL-UNNAMED would open the door, which is why such flags in a start script deserve a comment explaining who needs them.

Example · java
void main() throws Exception {
    // The class exists and is public, but its package is not exported by java.base
    Class<?> unsafe = Class.forName("jdk.internal.misc.Unsafe");
    IO.println(Modifier.isPublic(unsafe.getModifiers()));
    IO.println(String.class.getModule().isExported("jdk.internal.misc"));
    IO.println(String.class.getModule().isExported("java.util"));

    try {
        unsafe.getMethod("getUnsafe").invoke(null);
    } catch (IllegalAccessException e) {
        // The message ends with the unnamed module's identity, which changes per run
        IO.println(e.getMessage().replaceAll("@[0-9a-f]+", "@..."));
    }
}

// Output:
// true
// false
// true
// class Main cannot access class jdk.internal.misc.Unsafe (in module java.base) because module java.base does not export jdk.internal.misc to unnamed module @...

Compiling and running a modular application

The first two commands are the modular equivalent of javac -d and java -cp. The jdeps and jlink pair is useful for every service, modular or not, because it decides how much of the JDK goes into the container image.

Example · bash
# Layout: src/com.example.shop/module-info.java
#         src/com.example.shop/com/example/shop/App.java
javac -d out --module-source-path src --module com.example.shop

# Run from the module path: module name / main class
java --module-path out --module com.example.shop/com.example.shop.App

# Explore the JDK's own modules
java --list-modules
java --describe-module java.net.http

# For a class path service: which JDK modules does it need?
jdeps --ignore-missing-deps --print-module-deps target/shop.jar

# Build a runtime holding only those modules
jlink --add-modules java.base,java.net.http,java.sql --output build/runtime --strip-debug --no-header-files --no-man-pages

Discussion

  • Be the first to comment on this lesson.