Introduction to Java Platform Module System (JPMS)
The Java Platform Module System (JPMS), introduced in Java 9, provides a new way to structure Java applications and libraries through modules. The module-info.java file is the module descriptor that defines a module's structure, dependencies, and exports.
1. Basic Module Structure
Simple Module Example
// module-info.java
module com.example.myapp {
requires java.base; // Implicitly required, but shown for clarity
requires java.sql; // Dependency on SQL module
requires java.logging; // Dependency on logging module
exports com.example.myapp.api; // Export public API
exports com.example.myapp.model to com.example.ui;
opens com.example.myapp.internal; // Open for reflection
}
Corresponding Package Structure
src/ ├── com.example.myapp/ │ ├── module-info.java │ └── com/ │ └── example/ │ └── myapp/ │ ├── api/ │ │ ├── Calculator.java │ │ └── UserService.java │ ├── model/ │ │ ├── User.java │ │ └── Account.java │ ├── internal/ │ │ ├── DatabaseHelper.java │ │ └── CacheManager.java │ └── Main.java
Compilation and Execution
# Compile with modules javac -d out --module-source-path src $(find src -name "*.java") # Run the application java --module-path out -m com.example.myapp/com.example.myapp.Main # Create modular JAR jar --create --file myapp.jar --main-class com.example.myapp.Main -C out/com.example.myapp .
2. Module Directive Types
Requires Directives
module com.example.application {
// Basic dependency
requires java.sql;
// Transitive dependency - also available to modules requiring this module
requires transitive java.xml;
// Static dependency - required at compile time, optional at runtime
requires static java.compiler;
// Optional dependency for specific features
requires static com.thirdparty.optional;
}
Exports and Opens Directives
module com.example.library {
// Export package to all modules
exports com.example.library.api;
// Qualified export - only to specific modules
exports com.example.library.internal to com.example.framework;
// Open package for deep reflection
opens com.example.library.reflection;
// Qualified open - only to specific modules
opens com.example.library.serialization to com.example.serializer;
// Open entire module for reflection (use with caution)
open module com.example.library {
// Module directives go here
}
}
Uses and Provides Directives
module com.example.service {
requires java.base;
// Declare service consumption
uses com.example.spi.TranslationService;
uses java.util.spi.ResourceBundleProvider;
// Declare service implementation
provides com.example.spi.TranslationService
with com.example.service.EnglishTranslationService,
com.example.service.SpanishTranslationService;
provides java.util.spi.ResourceBundleProvider
with com.example.service.CustomResourceBundleProvider;
}
3. Complete Module Examples
Library Module
// module-info.java for a utility library
module com.example.utils {
// Basic dependencies
requires java.base;
requires java.logging;
// Transitive dependencies for users of this library
requires transitive java.json;
requires transitive org.apache.commons.lang3;
// Export public API packages
exports com.example.utils.collections;
exports com.example.utils.string;
exports com.example.utils.validation;
// Export to specific framework modules only
exports com.example.utils.internal.parsers to com.example.framework;
// Open for serialization frameworks
opens com.example.utils.serialization;
// Open internals to testing frameworks at runtime
opens com.example.utils.internal to org.junit.platform.commons;
}
Application Module
// module-info.java for a main application
module com.example.banking.app {
// Application dependencies
requires java.base;
requires java.sql;
requires java.logging;
requires transitive java.desktop; // For UI components
// Framework dependencies
requires transitive com.example.framework;
requires static com.example.monitoring; // Optional monitoring
// Third-party libraries
requires org.apache.commons.csv;
requires com.google.gson;
// Export application API (if this is a modular application framework)
exports com.example.banking.app.api;
// Open for dependency injection and ORM frameworks
opens com.example.banking.app.model to org.hibernate.orm;
opens com.example.banking.app.service to com.google.inject;
// Use services
uses com.example.banking.spi.ReportGenerator;
uses com.example.banking.spi.NotificationService;
// Provide implementations
provides com.example.banking.spi.ReportGenerator
with com.example.banking.app.reports.PdfReportGenerator;
provides com.example.banking.spi.NotificationService
with com.example.banking.app.notifications.EmailNotificationService;
}
Service Provider Module
// module-info.java for a service provider implementation
module com.example.database.driver {
requires java.base;
requires transitive java.sql;
// Service provider interface dependency
requires com.example.database.spi;
// Export the driver implementation (usually not needed for SPI)
// exports com.example.database.driver.impl;
// Provide the service implementation
provides com.example.database.spi.DatabaseDriver
with com.example.database.driver.CustomDatabaseDriver;
provides java.sql.Driver
with com.example.database.driver.JdbcDriverWrapper;
}
4. Advanced Module Patterns
Multi-Module Project Structure
Project Layout:
banking-system/ ├── api/ │ ├── src/ │ │ └── com.example.banking.api/ │ │ ├── module-info.java │ │ └── com/example/banking/api/ │ └── build.gradle ├── core/ │ ├── src/ │ │ └── com.example.banking.core/ │ │ ├── module-info.java │ │ └── com/example/banking/core/ │ └── build.gradle ├── web/ │ ├── src/ │ │ └── com.example.banking.web/ │ │ ├── module-info.java │ │ └── com/example/banking/web/ │ └── build.gradle └── app/ ├── src/ │ └── com.example.banking.app/ │ ├── module-info.java │ └── com/example/banking/app/ └── build.gradle
API Module:
// banking-system/api/src/module-info.java
module com.example.banking.api {
exports com.example.banking.api;
exports com.example.banking.api.model;
exports com.example.banking.api.services;
// SPI for extensibility
exports com.example.banking.api.spi;
uses com.example.banking.api.spi.AuditService;
uses com.example.banking.api.spi.ValidationService;
}
Core Module:
// banking-system/core/src/module-info.java
module com.example.banking.core {
requires transitive com.example.banking.api;
requires java.sql;
requires java.logging;
requires org.apache.commons.lang3;
exports com.example.banking.core.impl;
provides com.example.banking.api.spi.AuditService
with com.example.banking.core.audit.DatabaseAuditService;
provides com.example.banking.api.spi.ValidationService
with com.example.banking.core.validation.BusinessRuleValidator;
opens com.example.banking.core.entities to org.hibernate.orm;
}
Web Module:
// banking-system/web/src/module-info.java
module com.example.banking.web {
requires transitive com.example.banking.api;
requires com.example.banking.core;
requires java.base;
requires jakarta.servlet;
requires com.fasterxml.jackson.databind;
exports com.example.banking.web.controllers;
exports com.example.banking.web.dto;
opens com.example.banking.web.dto to com.fasterxml.jackson.databind;
opens com.example.banking.web.controllers to jakarta.servlet;
}
Application Module:
// banking-system/app/src/module-info.java
module com.example.banking.app {
requires com.example.banking.api;
requires com.example.banking.core;
requires com.example.banking.web;
requires java.base;
requires java.logging;
// Application starter
exports com.example.banking.app;
}
Optional Dependencies and Service Loading
// Module with optional features
module com.example.document.processor {
requires java.base;
requires transitive java.logging;
// Optional dependencies for different formats
requires static com.example.pdf.parser;
requires static com.example.word.processor;
requires static com.example.excel.reader;
// Core exports
exports com.example.document.processor.core;
// Service usage
uses com.example.document.processor.spi.DocumentParser;
// Conditional service provision
provides com.example.document.processor.spi.DocumentParser
with com.example.document.processor.core.FallbackParser;
}
Service Loader Implementation:
package com.example.document.processor.core;
import com.example.document.processor.spi.DocumentParser;
import java.util.Optional;
import java.util.ServiceLoader;
public class DocumentProcessor {
private final ServiceLoader<DocumentParser> parsers;
public DocumentProcessor() {
this.parsers = ServiceLoader.load(DocumentParser.class);
}
public Optional<DocumentParser> findParser(String fileType) {
return parsers.stream()
.filter(provider -> provider.get().supportsFormat(fileType))
.map(ServiceLoader.Provider::get)
.findFirst();
}
public void processDocument(String filePath) {
String fileType = getFileType(filePath);
Optional<DocumentParser> parser = findParser(fileType);
if (parser.isPresent()) {
parser.get().parse(filePath);
} else {
new FallbackParser().parse(filePath);
}
}
}
5. Migration and Compatibility
Automatic Module Names
When migrating non-modular JARs:
// For JARs without module-info.java, they become automatic modules
module com.example.legacy.app {
requires java.base;
// Automatic module names derived from JAR filename
requires commons.lang; // From commons-lang-3.12.0.jar
requires guava; // From guava-31.0.1.jar
requires slf4j.api; // From slf4j-api-1.7.36.jar
// Or use --module-path with existing JARs
}
Mixed Module Path and Class Path
// Module that works with classpath resources
module com.example.migration {
requires java.base;
// For accessing resources from classpath modules
opens com.example.migration.resources;
// For reflective access to classpath classes
opens com.example.migration.adapters;
}
Unnamed Module Access
// Techniques for accessing classpath resources from modules
public class ResourceLoader {
// Access resources in modular application
public InputStream getModuleResource(String resourcePath) {
return getClass().getModule()
.getResourceAsStream(resourcePath);
}
// Fallback for classpath resources
public InputStream getClasspathResource(String resourcePath) {
return getClass().getClassLoader()
.getResourceAsStream(resourcePath);
}
}
6. Testing with Modules
Test Module Structure
src/ ├── main/ │ └── java/ │ └── com.example.app/ │ ├── module-info.java │ └── com/example/app/ └── test/ └── java/ └── com.example.app.test/ ├── module-info.java └── com/example/app/test/
Main Module:
// src/main/java/module-info.java
module com.example.app {
requires java.base;
exports com.example.app.api;
// Open for testing
opens com.example.app.internal to com.example.app.test;
}
Test Module:
// src/test/java/module-info.java
open module com.example.app.test {
requires com.example.app; // Module under test
requires org.junit.jupiter.api; // Testing framework
requires org.mockito; // Mocking framework
requires org.assertj.core; // Assertion library
// Export test packages if needed
exports com.example.app.test;
}
Test Configuration with Build Tools
Maven Configuration:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>17</release>
<compilerArgs>
<arg>--module-path</arg>
<arg>${project.build.directory}/modules</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
<configuration>
<argLine>--module-path ${project.build.directory}/modules
--add-modules ALL-MODULE-PATH
--add-reads com.example.app=com.example.app.test
--patch-module com.example.app=${project.build.testOutputDirectory}</argLine>
</configuration>
</plugin>
</plugins>
</build>
7. Advanced Module Techniques
Dynamic Module Configuration
// Programmatic module access
public class ModuleUtils {
public static void configureModuleAccess() {
Module currentModule = ModuleUtils.class.getModule();
Module targetModule = findModule("com.example.target");
if (currentModule != null && targetModule != null) {
// Add reads relationship programmatically
currentModule.addReads(targetModule);
// Export packages dynamically (limited to unnamed/open modules)
if (currentModule.isNamed()) {
System.out.println("Module: " + currentModule.getName());
}
}
}
public static void analyzeModule(Module module) {
System.out.println("Module: " + module.getName());
System.out.println("Packages: " + module.getPackages());
System.out.println("Descriptors: " + module.getDescriptor());
module.getDescriptor().requires().forEach(req -> {
System.out.println("Requires: " + req.name() +
(req.modifiers().contains(Requires.Modifier.TRANSITIVE) ? " (transitive)" : ""));
});
}
private static Module findModule(String moduleName) {
return ModuleLayer.boot().findModule(moduleName).orElse(null);
}
}
Layer-based Module Architecture
// Advanced module layer configuration
public class ModuleLayerManager {
public static ModuleLayer createCustomLayer(Path modulePath,
Set<String> rootModules) {
Configuration configuration = ModuleLayer.boot()
.configuration()
.resolve(ModuleFinder.of(modulePath),
ModuleFinder.of(),
rootModules);
ClassLoader parentLoader = ClassLoader.getSystemClassLoader();
ModuleLayer layer = ModuleLayer.boot()
.defineModulesWithOneLoader(configuration, parentLoader);
return layer;
}
public static void loadPlugin(String pluginPath, String pluginModule) {
Path path = Paths.get(pluginPath);
Set<String> rootModules = Set.of(pluginModule);
ModuleLayer pluginLayer = createCustomLayer(path, rootModules);
// Use ServiceLoader in the new layer
ServiceLoader<Plugin> loader = ServiceLoader
.load(pluginLayer, Plugin.class);
loader.stream().forEach(provider -> {
Plugin plugin = provider.get();
plugin.initialize();
});
}
}
Resource Management in Modules
// Accessing resources in modular applications
public class ModularResourceLoader {
public Optional<URL> getResource(Module module, String resourcePath) {
try {
// Try module-relative path first
URL resource = module.getResource(resourcePath);
if (resource != null) {
return Optional.of(resource);
}
// Fallback to classloader
return Optional.ofNullable(
module.getClassLoader().getResource(resourcePath)
);
} catch (Exception e) {
return Optional.empty();
}
}
public List<URL> getAllResources(String resourceName) {
List<URL> resources = new ArrayList<>();
// Search in all modules
ModuleLayer.boot().modules().forEach(module -> {
getResource(module, resourceName).ifPresent(resources::add);
});
return resources;
}
public InputStream getResourceAsStream(String resourcePath) {
Module module = getClass().getModule();
return module.getResourceAsStream(resourcePath);
}
}
8. Common Patterns and Best Practices
Module Design Patterns
// 1. API Module Pattern
module com.example.library.api {
exports com.example.library.api;
exports com.example.library.api.spi;
uses com.example.library.api.spi.ExtensionPoint;
}
// 2. Implementation Module Pattern
module com.example.library.impl {
requires transitive com.example.library.api;
requires java.sql;
provides com.example.library.api.spi.ExtensionPoint
with com.example.library.impl.DefaultImplementation;
opens com.example.library.impl.internal to spring.core;
}
// 3. Application Module Pattern
module com.example.application {
requires com.example.library.api;
requires com.example.library.impl;
requires java.base;
requires java.logging;
uses com.example.library.api.spi.ExtensionPoint;
exports com.example.application.main;
}
// 4. Test Module Pattern
open module com.example.application.test {
requires com.example.application;
requires org.junit.jupiter;
requires org.mockito;
opens com.example.application to org.junit.platform.commons;
}
Migration Strategies
Incremental Migration:
// Step 1: Add module-info.java to root package
module com.example.legacyapp {
// Start with empty module, then gradually add requires
}
// Step 2: Identify dependencies
module com.example.legacyapp {
requires java.base;
requires java.sql;
requires java.logging;
// Use automatic modules for third-party JARs
requires commons.collections;
requires log4j;
}
// Step 3: Refactor exports
module com.example.legacyapp {
requires java.base;
requires java.sql;
exports com.example.legacyapp.api;
// Keep internal packages hidden
}
Common Pitfalls and Solutions
// Problem: Reflective access in modules
module com.example.reflection.issue {
requires java.base;
// Solution: Open packages for reflection
opens com.example.reflection.issue.model;
opens com.example.reflection.issue.serialization;
}
// Problem: Service loader not finding implementations
module com.example.service.consumer {
requires java.base;
// Solution: Ensure proper provides/uses declarations
uses com.example.spi.MyService;
}
module com.example.service.provider {
requires com.example.service.consumer;
provides com.example.spi.MyService
with com.example.service.provider.MyServiceImpl;
}
// Problem: Resource access
public class ResourceAccess {
// Wrong in modules:
// InputStream is = getClass().getResourceAsStream("/config.properties");
// Correct in modules:
InputStream is = getClass().getModule()
.getResourceAsStream("config.properties");
}
9. Tooling and Commands
Module-related Java Commands
# Compile modules javac -d out --module-source-path src $(find src -name "*.java") # List module details java --list-modules java --describe-module java.base # Run modular application java --module-path out -m com.example.app/com.example.app.Main # Create modular JAR jar --create --file app.jar --main-class com.example.app.Main -C out/com.example.app . # Analyze module dependencies jdeps --module-path out --module com.example.app # Generate module graph jdeps -dotoutput dots --module-path out --module com.example.app # JLink for custom runtime jlink --module-path $JAVA_HOME/jmods:out --add-modules com.example.app --output custom-runtime
Build Tool Integration
Gradle Configuration:
plugins {
id 'java'
id 'application'
}
java {
modularity.inferModulePath = true
}
dependencies {
implementation 'org.apache.commons:commons-lang3:3.12.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.2'
}
application {
mainModule = 'com.example.app'
mainClass = 'com.example.app.Main'
}
test {
useJUnitPlatform()
moduleOptions {
runOnClasspath = false
}
}
Summary
Key Benefits of Java Modules:
- Strong Encapsulation: Explicit control over exported APIs
- Reliable Configuration: Explicit dependency declaration
- Improved Security: Reduced attack surface
- Better Performance: Startup time and memory footprint improvements
- Scalability: Better support for large applications
Common Use Cases:
- Large Applications: Modular monoliths with clear boundaries
- Libraries: Well-defined public APIs with hidden internals
- Frameworks: Extensible architectures with service loading
- Microservices: Independent, self-contained modules
- Platform Development: Custom runtime images with jlink
Best Practices:
- Start with simple module declarations and gradually refine
- Use
requires transitivefor API dependencies - Open packages selectively for reflection
- Leverage service loading for extensibility
- Test modules with appropriate test configurations
- Use build tools that support modular development
The Module System represents a fundamental shift in how Java applications are structured, providing better encapsulation, more reliable configuration, and improved maintainability for large-scale applications.