The Java Platform Module System (JPMS) – Java 9 Explained
The Java Platform Module System (JPMS), also known as Project Jigsaw, is the most significant structural change to the Java platform since its creation. Introduced in Java 9, it allows you to organize code into named modules with explicit dependency declarations and controlled access. This solves long-standing problems with the Java classpath, classpath hell, and unwanted access to internal JDK APIs.
The Problem JPMS Solves
Before Java 9, any code could access any class from any JAR on the classpath. This caused several issues:
- JAR hell: conflicting library versions with no isolation
- Weak encapsulation: internal JDK classes (like
sun.misc.Unsafe) were accessible and widely used - Slow startup: the entire JDK was always available, even if you only needed 5% of it
- Large deployments: impossible to ship a minimal Java runtime
Key Concepts
| Concept | Meaning |
|---|---|
| module | A named unit of code with a module descriptor |
| module-info.java | The module descriptor — declares the module's name, dependencies, and exports |
| requires | Declares a dependency on another module |
| exports | Makes a package accessible to other modules |
| opens | Allows deep reflection (needed for frameworks like Spring, Hibernate) |
| provides / uses | Service provider interface declarations |
Project Structure
my-app/
├── com.java9r.api/
│ ├── module-info.java
│ └── com/java9r/api/
│ ├── ProductService.java
│ └── Product.java
│
└── com.java9r.app/
├── module-info.java
└── com/java9r/app/
└── Main.java
Module 1: com.java9r.api (the library module)
// com.java9r.api/module-info.java
module com.java9r.api {
// Export these packages so other modules can use them
exports com.java9r.api;
// Requires the logging module from JDK
requires java.logging;
}
// com/java9r/api/Product.java
package com.java9r.api;
public class Product {
private final String id;
private final String name;
private final double price;
public Product(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
public String getId() { return id; }
public String getName() { return name; }
public double getPrice() { return price; }
@Override
public String toString() {
return "Product{id='" + id + "', name='" + name + "', price=" + price + "}";
}
}
// com/java9r/api/ProductService.java
package com.java9r.api;
import java.util.List;
import java.util.logging.Logger;
public class ProductService {
private static final Logger logger = Logger.getLogger(ProductService.class.getName());
public List<Product> getAllProducts() {
logger.info("Fetching all products");
return List.of(
new Product("P001", "Laptop", 75000.0),
new Product("P002", "Smartphone", 25000.0),
new Product("P003", "Tablet", 35000.0)
);
}
public Product findById(String id) {
return getAllProducts().stream()
.filter(p -> p.getId().equals(id))
.findFirst()
.orElse(null);
}
}
Module 2: com.java9r.app (the application module)
// com.java9r.app/module-info.java
module com.java9r.app {
// Depend on the API module
requires com.java9r.api;
// JDK modules needed
requires java.logging;
}
// com/java9r/app/Main.java
package com.java9r.app;
import com.java9r.api.Product;
import com.java9r.api.ProductService;
public class Main {
public static void main(String[] args) {
ProductService service = new ProductService();
System.out.println("=== All Products ===");
service.getAllProducts().forEach(System.out::println);
System.out.println("\n=== Find by ID ===");
Product found = service.findById("P002");
System.out.println(found != null ? found : "Not found");
}
}
Compile and Run Modular Code
# Step 1: Compile the API module
javac -d out/com.java9r.api \
com.java9r.api/module-info.java \
com.java9r.api/com/java9r/api/*.java
# Step 2: Compile the app module, providing the API module on the module path
javac --module-path out \
-d out/com.java9r.app \
com.java9r.app/module-info.java \
com.java9r.app/com/java9r/app/*.java
# Step 3: Run with module path
java --module-path out --module com.java9r.app/com.java9r.app.Main
Expected Output
=== All Products ===
Product{id='P001', name='Laptop', price=75000.0}
Product{id='P002', name='Smartphone', price=25000.0}
Product{id='P003', name='Tablet', price=35000.0}
=== Find by ID ===
Product{id='P002', name='Smartphone', price=25000.0}
What Happens Without exports?
// If com.java9r.api/module-info.java had NO exports:
module com.java9r.api {
// exports com.java9r.api; <-- commented out
}
// Then in Main.java:
import com.java9r.api.Product; // COMPILE ERROR!
// "package com.java9r.api is not visible"
// "(package com.java9r.api is declared in module com.java9r.api, which does not export it)"
This is strong encapsulation — packages are hidden by default and must be explicitly exported.
JDK Modules (Built-in)
# View all JDK modules:
java --list-modules
# Key JDK modules:
# java.base – core classes (always available, never needs to be declared)
# java.sql – JDBC
# java.logging – java.util.logging
# java.desktop – Swing, AWT
# java.net.http – HTTP Client (Java 11+)
# jdk.jshell – JShell API
Summary
The Java module system solves the classpath hell and weak encapsulation problems that plagued Java applications for decades. By requiring explicit exports declarations, modules hide implementation details by default. requires makes dependencies explicit and verifiable at startup. The module system is especially valuable for large applications and microservices where you want a minimal, optimized Java runtime using jlink. While migrating existing projects to modules requires effort, all new Java projects can benefit from defining a module-info.java from day one.
Comments