The module system (JPMS)

Java · Advanced · 12 min

Modules give strong encapsulation for libraries and the JDK itself, and explain the access errors you meet on modern Java.

Since Java 9 the JDK is split into modules (java.base, java.sql, java.net.http, ...), and your code can be too. A module is a set of packages plus a descriptor, module-info.java, at the root of the source tree:

module com.example.shop {
    requires java.net.http;
    requires transitive com.example.money;
    exports com.example.shop.api;
    opens com.example.shop.model to com.fasterxml.jackson.databind;
}
  • requires — this module depends on another; missing modules fail at startup, not halfway through.
  • requires transitive — modules that require you also get this dependency (use it when your API exposes its types).
  • exports — packages other modules may compile against. Everything else, even public classes, is strongly encapsulated.
  • opens — allows deep reflection at runtime (frameworks binding to private fields) without exporting the package for compilation.
  • uses / provides ... with — service loading via ServiceLoader.

Code on the classpath lives in the unnamed module, which reads every module. That is why most applications still run unmodularised. Plain JARs placed on the module path become automatic modules named after the JAR (or the Automatic-Module-Name manifest entry).

Even if you never write module-info.java, JPMS affects you: since Java 16–17, reflective access to JDK internals is denied by default. Errors like InaccessibleObjectException ... does not "opens java.lang" to unnamed module come from this; the fix is upgrading the library, or as a last resort --add-opens on the command line.

Modules also enable jlink, which builds a trimmed custom runtime containing only the modules your app needs — handy for small container images.

Example

// File: src/com.example.greet/module-info.java
module com.example.greet {
    requires java.logging;
    exports com.example.greet.api;        // public API
    // com.example.greet.internal is NOT exported: inaccessible to other modules
}

// File: src/com.example.greet/com/example/greet/api/Greeter.java
package com.example.greet.api;

import java.util.logging.Logger;

public class Greeter {
    private static final Logger LOG = Logger.getLogger(Greeter.class.getName());
    public static String greet(String name) {
        LOG.fine("greeting " + name);
        return "Hello, " + name;
    }
}

// Compile, then build a minimal runtime image:
//   javac -d out --module-source-path src -m com.example.greet
//   jlink --module-path out --add-modules com.example.greet --output greet-runtime

Remember

  • module-info.java declares requires, exports and opens
  • Unexported packages are inaccessible, even for public classes
  • Classpath code runs in the unnamed module; JDK internals are closed to reflection by default

Watch out for

  • Silencing reflective-access errors with blanket --add-opens flags instead of upgrading the offending library.

Check yourself

  1. A framework must use reflection on private fields in your package but other modules shouldn't compile against it. What do you declare?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/javase/specs/jls/se21/html/jls-7.html#jls-7.7

The module system (JPMS) · Java · Topolearn