Java Module System (JPMS)
Master the Java Platform Module System — module-info.java, exports, requires, opens, services, and modular application structure.
Why Modules?
Before the Java Platform Module System (JPMS, Java 9+), the JDK itself was one massive classpath JAR — rt.jar — and there was no way to prevent other code from using internal JDK classes like sun.misc.Unsafe. JPMS introduces:
- Strong encapsulation — packages are hidden by default; only exported packages are accessible
- Explicit dependencies — each module declares exactly what it needs
- Reliable configuration — missing or duplicate modules are detected at startup, not at runtime
- Reduced footprint — custom JVM images can include only the modules needed
Module Basics
A module is a named group of packages declared in module-info.java at the module root:
src/
└── com.example.app/
├── module-info.java
└── com/example/app/
└── Main.java
// module-info.java — must be at the source root of the module
module com.example.app {
requires java.net.http; // depend on JDK HTTP client module
requires com.example.utils; // depend on another app module
exports com.example.app.api; // make this package public to other modules
}
A Complete Multi-Module Example
project/
├── modules/
│ ├── com.example.api/
│ │ ├── module-info.java
│ │ └── com/example/api/
│ │ └── UserService.java
│ ├── com.example.impl/
│ │ ├── module-info.java
│ │ └── com/example/impl/
│ │ └── UserServiceImpl.java
│ └── com.example.app/
│ ├── module-info.java
│ └── com/example/app/
│ └── Main.java
API module — defines the contract
// modules/com.example.api/module-info.java
module com.example.api {
exports com.example.api; // expose the interface
}
// com/example/api/UserService.java
package com.example.api;
import java.util.List;
import java.util.Optional;
public interface UserService {
Optional<User> findById(long id);
List<User> findAll();
User save(User user);
}
public record User(long id, String name, String email) {}
Implementation module
// modules/com.example.impl/module-info.java
module com.example.impl {
requires com.example.api; // depends on the API
exports com.example.impl; // expose the implementation class
}
// com/example/impl/UserServiceImpl.java
package com.example.impl;
import com.example.api.User;
import com.example.api.UserService;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
public class UserServiceImpl implements UserService {
private final Map<Long, User> store = new HashMap<>();
private final AtomicLong ids = new AtomicLong(1);
@Override public Optional<User> findById(long id) {
return Optional.ofNullable(store.get(id));
}
@Override public List<User> findAll() {
return List.copyOf(store.values());
}
@Override public User save(User user) {
var saved = new User(ids.getAndIncrement(), user.name(), user.email());
store.put(saved.id(), saved);
return saved;
}
}
Application module
// modules/com.example.app/module-info.java
module com.example.app {
requires com.example.api;
requires com.example.impl;
}
// com/example/app/Main.java
package com.example.app;
import com.example.api.User;
import com.example.api.UserService;
import com.example.impl.UserServiceImpl;
public class Main {
public static void main(String[] args) {
UserService svc = new UserServiceImpl();
svc.save(new User(0, "Alice", "[email protected]"));
svc.save(new User(0, "Bob", "[email protected]"));
svc.findAll().forEach(System.out::println);
// User[id=1, name=Alice, [email protected]]
// User[id=2, name=Bob, [email protected]]
}
}
exports Directive
module com.example.lib {
// Export to everyone
exports com.example.lib.api;
// Qualified export — only to specific modules
exports com.example.lib.internal to com.example.app, com.example.tests;
// Internal packages are hidden by default — no exports needed
// com.example.lib.impl is completely inaccessible to other modules
}
opens Directive — Reflection Access
Frameworks like Spring, Jackson, and Hibernate need deep reflective access (setting private fields, invoking private constructors). Use opens:
module com.example.model {
requires java.base;
// Open for reflection at runtime (Jackson, Hibernate, Spring)
opens com.example.model.entity to com.fasterxml.jackson.databind;
opens com.example.model.dto to org.springframework.core;
// Open to everyone — use sparingly
opens com.example.model.config;
// Exports the package for normal compilation use
exports com.example.model.entity;
exports com.example.model.dto;
}
Services — Decoupled Module Communication
The service loader mechanism lets modules provide and consume services without compile-time dependencies:
// API module — declares the service interface
module com.example.api {
exports com.example.api;
uses com.example.api.PaymentProcessor; // declares it consumes this service
}
// Provider module — implements the service
module com.example.stripe {
requires com.example.api;
provides com.example.api.PaymentProcessor
with com.example.stripe.StripeProcessor; // registers the implementation
}
// Consumer — load at runtime via ServiceLoader
import java.util.ServiceLoader;
import com.example.api.PaymentProcessor;
ServiceLoader<PaymentProcessor> loader = ServiceLoader.load(PaymentProcessor.class);
PaymentProcessor processor = loader.findFirst()
.orElseThrow(() -> new RuntimeException("No PaymentProcessor found"));
processor.charge(9.99, "USD");
Compiling and Running Modules
# Compile each module
javac -d out/com.example.api \
--module-source-path modules \
modules/com.example.api/module-info.java \
modules/com.example.api/com/example/api/*.java
javac -d out/com.example.impl \
--module-path out \
modules/com.example.impl/module-info.java \
modules/com.example.impl/com/example/impl/*.java
javac -d out/com.example.app \
--module-path out \
modules/com.example.app/module-info.java \
modules/com.example.app/com/example/app/*.java
# Run the application
java --module-path out -m com.example.app/com.example.app.Main
jlink — Custom Runtime Images
jlink creates a minimal JVM image containing only the modules your application needs:
# Find which JDK modules your app uses
jdeps --module-path out -m com.example.app
# Build a custom JRE containing only required modules
jlink \
--module-path $JAVA_HOME/jmods:out \
--add-modules com.example.app,com.example.impl,com.example.api \
--output custom-jre \
--compress zip-6 \
--no-header-files \
--no-man-pages
# Run with the custom JRE (no JDK needed on target machine)
custom-jre/bin/java -m com.example.app/com.example.app.Main
A typical Spring Boot app cut to only needed modules can shrink from a 200 MB JDK to a 40-60 MB runtime image.
Module Descriptor Quick Reference
module com.example.full {
// Declare dependencies
requires java.sql; // compile + runtime
requires transitive java.logging; // transitive: consumers of this module also get java.logging
requires static java.compiler; // optional at runtime (only needed at compile time)
// Expose packages for compilation and runtime
exports com.example.full.api;
exports com.example.full.spi to com.example.plugins; // qualified
// Expose packages for deep reflection
opens com.example.full.model;
opens com.example.full.model to com.fasterxml.jackson.databind; // qualified
// Service declarations
uses com.example.full.spi.Plugin;
provides com.example.full.spi.Plugin with com.example.full.DefaultPlugin;
}