12.5 Java Module System (JPMS)
What Is the Module System?
Introduced in Java 9(Project Jigsaw), the Java Platform Module System (JPMS) adds a new level of encapsulation above packages. A module is a named, self-describing collection of packages that explicitly declares what it requires and what it exposes.
Before modules, all public classes across all JARs on the classpath were accessible to each other — there was no real way to hide internal implementation details from other libraries. JPMS solves this.
1. Why Modules?
Problems with the pre-module classpath:
- Accessibility: Any
publicclass was accessible from anywhere — no internal encapsulation between JARs - JAR Hell: Multiple versions of the same library on the classpath caused unpredictable behavior
- Bloated JRE: The entire JDK (100+ packages) was always loaded, even for small apps
- No explicit dependencies: You couldn't tell what a JAR needed without running it
JPMS benefits:
- Strong encapsulation — control exactly what packages are visible
- Reliable dependencies — explicit
requiresdeclarations - Smaller deployments — include only the modules you need (
jlink) - Improved security — internal APIs cannot be accessed accidentally
2. module-info.java
Every module has a module-info.java file at the root of its source directory:
// src/com.example.myapp/module-info.java
module com.example.myapp {
// Dependencies: other modules this module needs
requires java.base; // always implicit — java.lang, java.util, etc.
requires java.net.http; // HTTP client module
requires java.sql; // JDBC module
// Exported packages: which packages OTHER modules can use
exports com.example.myapp.api;
exports com.example.myapp.model;
// com.example.myapp.internal is NOT exported — stays private
// Opens packages for deep reflection (e.g., frameworks like Spring/Hibernate)
opens com.example.myapp.model to com.fasterxml.jackson.databind;
}
3. Core Directives
requires — Declare Dependencies
module com.example.service {
requires java.logging; // normal dependency
requires transitive java.sql; // transitive: modules depending on THIS module
// also get access to java.sql
requires static java.annotation; // compile-time only (optional at runtime)
}
requires transitive— "I expose this module's API in my own API, so my callers need it too."
exports — Control Package Visibility
module com.example.library {
// Export to everyone
exports com.example.library.api;
// Qualified export — only specific modules can access
exports com.example.library.internal to com.example.tests, com.example.tools;
// com.example.library.impl is NOT exported — completely hidden
}
opens — Allow Deep Reflection
exports controls compile-time and runtime access. opens additionally allows reflection to access private members (needed by frameworks like Spring, Hibernate, Jackson):
module com.example.webapp {
exports com.example.webapp.controller; // public API accessible
// Open for reflection (Jackson needs to set private fields)
opens com.example.webapp.dto to com.fasterxml.jackson.databind;
// Open everything to a testing framework
opens com.example.webapp.service to org.mockito;
}
uses and provides — Service Loader
// A service consumer module
module com.example.app {
uses com.example.api.PaymentService; // declares use of a service interface
}
// A service provider module
module com.example.stripe {
requires com.example.api;
provides com.example.api.PaymentService
with com.example.stripe.StripePaymentService; // implementation
}
4. Module Types
| Type | Description | Example |
|---|---|---|
| Named module | Has module-info.java, on the module path | Your own modules |
| Automatic module | JAR on module path without module-info.java | Legacy 3rd-party JARs |
| Unnamed module | Everything on the classpath | Traditional classpath apps |
| Platform module | Built-in JDK modules | java.base, java.sql |
5. Multi-Module Project Structure
myproject/
├── src/
│ ├── com.example.common/
│ │ ├── module-info.java
│ │ └── com/example/common/
│ │ ├── model/
│ │ │ └── User.java
│ │ └── util/
│ │ └── StringUtils.java
│ │
│ ├── com.example.service/
│ │ ├── module-info.java
│ │ └── com/example/service/
│ │ └── UserService.java
│ │
│ └── com.example.app/
│ ├── module-info.java
│ └── com/example/app/
│ └── Main.java
// com.example.common/module-info.java
module com.example.common {
exports com.example.common.model;
exports com.example.common.util;
}
// com.example.service/module-info.java
module com.example.service {
requires com.example.common;
exports com.example.service;
}
// com.example.app/module-info.java
module com.example.app {
requires com.example.common;
requires com.example.service;
}
6. Compiling and Running Modular Code
# Compile all modules
javac -d out \
--module-source-path src \
-m com.example.common,com.example.service,com.example.app
# Run the application
java --module-path out -m com.example.app/com.example.app.Main
# Create a minimal custom runtime image (jlink)
jlink \
--module-path $JAVA_HOME/jmods:out \
--add-modules com.example.app \
--output myapp-runtime \
--launcher start=com.example.app/com.example.app.Main
# Run the custom image
./myapp-runtime/bin/start
7. JDK Platform Modules
The JDK itself is organized into modules. Common ones:
| Module | Contents |
|---|---|
java.base | Core Java (java.lang, java.util, java.io, etc.) |
java.sql | JDBC |
java.net.http | HTTP Client (Java 11+) |
java.logging | java.util.logging |
java.xml | JAXP XML processing |
java.desktop | AWT, Swing |
javafx.* | JavaFX UI framework |
jdk.jshell | JShell REPL |
View all available modules:
java --list-modules
Most applications don't need to use JPMS directly. Spring Boot, Jakarta EE, and similar frameworks handle module compatibility. You'll encounter JPMS mainly when:
- Building a library that needs strong API encapsulation
- Creating a custom JRE image with
jlink - Working with frameworks that use deep reflection (and need
opens)
Automatic modules make migration easier: putting a legacy JAR on the --module-path gives it a module name derived from its filename. This lets modular code depend on non-modular JARs during migration.
--add-opens flag: When a framework tries to reflect into a module that doesn't opens the package, you'll see InaccessibleObjectException. Fix it by adding to the JVM arguments:
--add-opens java.base/java.lang=ALL-UNNAMED
This is a common workaround when using older libraries with Java 17+.