Introduction to Java 9 Modularity
Java 9 introduced a fundamental change to the platform's architecture with the Java Platform Module System (JPMS), developed under Project Jigsaw. This system addresses long-standing issues in Java development, particularly classspath complexity and the lack of strong encapsulation at the module level.
The Problem: Classpath Hell
Traditional Java applications suffer from "Jar Hell" - similar to Windows' DLL Hell - where managing dependencies becomes complex with numerous JAR files on the classpath. This leads to:
- Long and unwieldy classpath strings
- Difficulty in managing dependency versions
- No clear way to enforce API boundaries
- Bloated deployments for smaller devices
Getting Started: First Module
Prerequisites
Ensure you're running Java 9 or later:
java -version
java version "9"
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
Creating a Basic Module
Create a simple Java class:
// src/com/example/launcher/App.java
package com.example.launcher;
public class App {
public static void main(String[] args) {
System.out.println("Welcome to Java modules!");
}
}
Create the module descriptor:
// src/module-info.java
module first.module {}
Compilation and Packaging
# Compile the module
javac -d target/classes src/module-info.java src/com/example/launcher/App.java
# Package as modular JAR
jar --create --file lib/first-module.jar --main-class com.example.launcher.App -C target/classes .
Running the Module
# Execute using module path
java --module-path lib -m first.module
Module System Fundamentals
Module Hierarchy
Java introduces a new layer in the package hierarchy:
module → package → class/interface
A module is:
- A named collection of packages
- A self-decsribing unit with explicit dependencies
- A container that controls which packages are accessible externally
Creating an Exporting Module
// module-info.java
module com.example.api {
exports com.example.api.core;
exports com.example.api.utils;
}
Only exported packages are accessible to other modules. Non-exported packages remain strongly encapsulated.
Module Dependencies
// Consumer module
module com.example.app {
requires com.example.api;
requires java.sql; // Standard module
}
// API module with its own dependency
module com.example.api {
requires com.example.core;
exports com.example.api.core;
}
Built-in Modules
The JDK itself is modularized. The java.base module contains fundamental classes:
module java.base {
exports java.lang;
exports java.io;
exports java.util;
// ... more core packages
}
All modules automatically require java.base, similar to how all code implicitly imports java.lang.*.
Reliable Configuration
The module system validates dependencies at compile and runtime:
- Compile-time: Detects missing required modules
- Runtime: Ensures all required modules are present
- No implicit dependencies: All dependencies must be explicit
Strong Encapsulation
Modules enforce strict access control:
// Attempting to access non-exported package
module com.example.consumer {
requires com.example.provider;
}
// If com.example.provider doesn't export com.example.provider.internal:
// Compile-time error: package com.example.provider.internal is not visible
Exploring Built-in Modules
List all available modules:
java --list-modules
Inspect a specific module's contents:
jmod list $JAVA_HOME/jmods/java.base.jmod
Practical Example: Multi-module Application
Create a Service Module
// service/src/com/example/service/GreetingService.java
package com.example.service;
public class GreetingService {
public void greet(String name) {
System.out.println("Hello, " + name + "!");
}
}
// service/src/module-info.java
module com.example.service {
exports com.example.service;
}
Create Consumer Module
// consumer/src/com/example/consumer/Main.java
package com.example.consumer;
import com.example.service.GreetingService;
public class Main {
public static void main(String[] args) {
new GreetingService().greet("modular world");
}
}
// consumer/src/module-info.java
module com.example.consumer {
requires com.example.service;
}
Build and Run
# Compile service module
javac -d service/target/classes service/src/module-info.java service/src/com/example/service/GreetingService.java
jar --create --file service/lib/service.jar -C service/target/classes .
# Compile consumer module
javac --module-path service/lib -d consumer/target/classes consumer/src/module-info.java consumer/src/com/example/consumer/Main.java
jar --create --file consumer/lib/consumer.jar --module-path service/lib --main-class com.example.consumer.Main -C consumer/target/classes .
# Run application
java --module-path service/lib:consumer/lib -m com.example.consumer
Module System Tools
jlink: Custom Runtime Images
Create optimized JRE containing only needed modules:
# Create minimal JRE with java.base only
jlink --module-path $JAVA_HOME/jmods --add-modules java.base --output custom-jre
# Size comparison
du -sh $JAVA_HOME custom-jre
# 493M /path/to/jdk-9
# 35M custom-jre
# Include application modules
jlink --module-path $JAVA_HOME/jmods:service/lib:consumer/lib \
--add-modules com.example.consumer \
--output app-runtime
# Create launcher
jlink --module-path $JAVA_HOME/jmods:service/lib:consumer/lib \
--add-modules com.example.consumer \
--launcher runapp=com.example.consumer \
--output app-runtime
jdeps: Dependency Analysis
jdeps --module-path service/lib consumer/lib/consumer.jar
jmod: Module Manipulation
# Create jmod file
jmod create --class-path consumer/lib/consumer.jar consumer.jmod
# List contents
jmod list consumer.jmod
jdeprscan: Deprecated API Scanner
jdeprscan library.jar
Key Concepts Summary
- Module Descriptor:
module-info.javadefines module structure - Modular JAR: Standard JAR containing module descriptor
- Reliable Configuration: Explicit dependency validation
- Strong Encapsulation: Export-based access control
- Readable Modules: Established dependency relationships
Modules vs Traditional JARs
| Aspect | JAR | Module |
|---|---|---|
| Structure | Flat collection of classes | Structured with explicit exports |
| Dependencies | Implicit via classpath | Explicit requires clauses |
| Encapsulation | Package-level only | Module-level control |
| Configuration | Manual classpath management | Automatic module resolution |
Practical Applications
- Custom Runtimes: Deploy minimal JREs for containers/embedded systems
- Plugin Architectures: Anforce clean API boundaries between components
- Microservices: Optimize deployment size and startup time
- Library Development: Provide stable, versioned APIs with clear contracts
Important Considerations
- Circular dependencies are prohibited between modules
- Modern IDEs require updates for full module-path support
- Modular JARs can still be used on classpath (as traditional JARs)
- Migration to modules is incremental - not an all-or-nothing change