Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9 under Project Jigsaw, adds a module as a new, higher-level unit of code organization above the package, allowing a JAR-like artifact to explicitly declare which packages it exports for use by other modules, which modules it depends on, and which packages it keeps entirely internal and inaccessible from outside — closing a long-standing gap in the classpath model where every public class in every JAR on the classpath was accessible to every other JAR, regardless of whether that access was intended. JPMS was also the mechanism used to decompose the JDK itself, which had grown into one monolithic rt.jar, into roughly 90 separate modules, enabling smaller custom runtime images and explicit internal-API encapsulation (notably restricting access to sun.* and other internal packages that many libraries had relied on despite warnings). This entry covers the classpath problems JPMS solves, module-info.java syntax and the requires/exports/opens directives, the distinction between the classpath and module path including automatic and unnamed modules, and jlink custom runtime images.
Motivation — The Classpath's Two Core Problems
// ── The classpath problem: weak encapsulation ───────────────────────────
// Before modules, ANY public class on the classpath is reachable from anywhere:
package com.mylib.internal;
public class InternalHelper { // "public" but documented as internal-use-only
public static void doSensitiveThing() { /* ... */ }
}
// Any other JAR on the classpath could legally call this, despite documentation:
// com.mylib.internal.InternalHelper.doSensitiveThing(); // compiles fine pre-Java 9
// ── The classpath problem: missing dependency surfaces only at runtime ──
// No declared dependency information — a missing JAR fails only when the
// missing class is actually loaded and used, deep into program execution:
// Exception in thread "main" java.lang.NoClassDefFoundError: com/somelib/SomeClass
// ── JPMS: a module explicitly declares its boundary ─────────────────────
// module-info.java at the root of the module's source:
module com.mylib {
exports com.mylib.api; // only this package is visible to other modules
// com.mylib.internal is NOT exported — invisible outside this module, ENFORCED
requires java.sql; // explicit, checked dependency
}
// Another module trying to access the unexported internal package:
// import com.mylib.internal.InternalHelper; // COMPILE ERROR:
// "package com.mylib.internal is not visible — it is not exported"module-info.java Directives — requires, exports, and opens
// ── requires — explicit, verified dependency ────────────────────────────
module com.myapp {
requires java.sql; // must be present, checked at compile+launch time
requires com.fasterxml.jackson.databind;
}
// ── exports — unqualified, visible to any module that requires this one ─
module com.mylib.core {
exports com.mylib.core.api; // public API — visible to all requiring modules
// com.mylib.core.impl is implicitly NOT exported — fully encapsulated
}
// ── exports ... to ... — qualified export, restricted to named modules ──
module com.mylib.core {
exports com.mylib.core.internalapi to com.mylib.testsupport, com.mylib.adminconsole;
// only these two named modules can see this package — everyone else cannot
}
// ── opens — grants reflective access without compile-time accessibility ─
module com.myapp.model {
exports com.myapp.model; // normal compile-time API access
opens com.myapp.model.entities to org.hibernate.orm.core;
// Hibernate can reflectively read/write private fields on entities,
// even though entities package is NOT exported for normal compile-time use
}
// Whole-module shortcut — opens every package in the module reflectively:
open module com.myapp.legacy {
requires java.base;
// every package in this module is open to reflection by any module
}
// ── requires transitive — propagates a dependency to downstream consumers
module com.mylib.api {
requires transitive com.mylib.types; // com.mylib.types appears in this API's signatures
exports com.mylib.api;
}
module com.consumer {
requires com.mylib.api; // automatically also gets access to com.mylib.types' exports,
// without separately declaring "requires com.mylib.types"
}Module Path vs Classpath, Automatic Modules, and jlink
# ── Classic classpath — unrestricted, no module declarations needed ────
javac -cp libs/*.jar -d out src/com/myapp/Main.java
java -cp out:libs/*.jar com.myapp.Main
# Works exactly as it always did — full backward compatibility preserved
# ── Module path — JARs treated as modules ───────────────────────────────
javac -p mods -d out src/com.myapp/module-info.java src/com.myapp/com/myapp/Main.java
java -p out:mods -m com.myapp/com.myapp.Main
# ── Automatic modules — non-modularized JAR placed on the module path ──
# legacy-lib.jar has no module-info.class, but is placed on the module path:
java -p mods:legacy-lib.jar -m com.myapp/com.myapp.Main
# JVM derives a module name (e.g. "legacy.lib" from the filename) and treats
# the WHOLE jar as exporting everything and requiring everything — interim bridge
// module-info.java referencing an automatic module by its derived name:
module com.myapp {
requires legacy.lib; // works even though legacy-lib.jar has no module-info
}
# ── jlink — building a minimal custom runtime image ─────────────────────
jlink --module-path $JAVA_HOME/jmods:mods \
--add-modules com.myapp \
--output custom-runtime \
--launcher run=com.myapp/com.myapp.Main \
--strip-debug --no-header-files --no-man-pages
# Resulting "custom-runtime" directory is a self-contained, executable JVM
# image containing only java.base + whatever com.myapp's module-info actually
# requires (transitively) — often a fraction of a full JDK install's size
./custom-runtime/bin/run