The Java Platform Module System (JPMS), introduced in Java 9, provides a new way to package and organize Java applications and libraries. It addresses issues like the "JAR hell" and improves application security, maintainability, and performance.
1. Module System Basics
What is a Module?
A module is a self-contained unit of code that:
- Has a unique name
- Declares its dependencies
- Exports specific packages
- Hides implementation details
Key Benefits
- Strong Encapsulation - Explicit control over what's exposed
- Reliable Configuration - Explicit dependencies
- Improved Security - Better access control
- Better Performance - Faster startup and smaller footprint
- Scalability - Only required modules are loaded
2. Module Descriptor (module-info.java)
Basic Module Structure
my-module/ ├── src/ │ └── module-info.java │ └── com/ │ └── example/ │ └── MyClass.java └── out/ └── my-module.jar
Simple Module Declaration
// module-info.java module com.example.mymodule { // Module declaration body } 3. Module Directives
requires - Declaring Dependencies
module com.example.app { // Required modules requires java.base; // Implicitly required by all modules requires java.sql; requires java.logging; // Optional dependency (not resolved at compile time) requires static java.xml; // Transitive dependency (available to users of this module) requires transitive java.desktop; } exports - Exposing Packages
module com.example.library { // Export specific packages exports com.example.library.api; exports com.example.library.utils; // Export to specific modules only exports com.example.library.internal to com.example.friend; // Package not exported (hidden) // com.example.library.internal.impl - not accessible outside } opens - Reflection Access
module com.example.reflective { // Open package for reflection (all modules) opens com.example.reflective.entities; // Open package to specific modules only opens com.example.reflective.internal to spring.core, hibernate; // Open entire module for reflection open module com.example.openmodule { requires java.sql; exports com.example.openmodule.api; } } uses and provides - Service Loading
module com.example.service.consumer { // Declare service dependency uses com.example.spi.TranslationService; requires com.example.spi; } module com.example.service.provider { // Provide service implementation provides com.example.spi.TranslationService with com.example.provider.EnglishTranslationService, com.example.provider.SpanishTranslationService; requires com.example.spi; exports com.example.provider; } 4. Complete Examples
Example 1: Simple Application Module
// module-info.java module com.example.contacts { requires java.base; requires java.sql; requires java.logging; requires transitive java.desktop; exports com.example.contacts.api; exports com.example.contacts.model; opens com.example.contacts.entities to hibernate.core; uses com.example.contacts.spi.StorageService; } Corresponding Package Structure
// com/example/contacts/api/ContactService.java package com.example.contacts.api; import com.example.contacts.model.Contact; import java.util.List; public class ContactService { public List<Contact> findAll() { return List.of( new Contact("John", "[email protected]"), new Contact("Jane", "[email protected]") ); } } // com/example/contacts/model/Contact.java package com.example.contacts.model; public class Contact { private final String name; private final String email; public Contact(String name, String email) { this.name = name; this.email = email; } // Getters public String getName() { return name; } public String getEmail() { return email; } } // com/example/contacts/internal/DatabaseHelper.java package com.example.contacts.internal; // This class is not exported - internal implementation class DatabaseHelper { void connect() { // Database connection logic } } Example 2: Multi-Module Application
Module 1: Domain Layer
// domain/module-info.java module com.example.domain { exports com.example.domain.model; exports com.example.domain.repository; // Service provider interface exports com.example.domain.spi; } // domain/com/example/domain/model/User.java package com.example.domain.model; public class User { private final String username; private final String email; public User(String username, String email) { this.username = username; this.email = email; } public String getUsername() { return username; } public String getEmail() { return email; } } // domain/com/example/domain/repository/UserRepository.java package com.example.domain.repository; import com.example.domain.model.User; import java.util.Optional; public interface UserRepository { Optional<User> findByUsername(String username); void save(User user); } // domain/com/example/domain/spi/IdGenerator.java package com.example.domain.spi; public interface IdGenerator { String generate(); } Module 2: Persistence Layer
// persistence/module-info.java module com.example.persistence { requires transitive com.example.domain; requires java.sql; provides com.example.domain.repository.UserRepository with com.example.persistence.jpa.JpaUserRepository; provides com.example.domain.spi.IdGenerator with com.example.persistence.uuid.UuidGenerator; exports com.example.persistence.config; } // persistence/com/example/persistence/jpa/JpaUserRepository.java package com.example.persistence.jpa; import com.example.domain.model.User; import com.example.domain.repository.UserRepository; import java.util.Optional; public class JpaUserRepository implements UserRepository { @Override public Optional<User> findByUsername(String username) { // JPA implementation return Optional.of(new User(username, username + "@example.com")); } @Override public void save(User user) { // Save to database System.out.println("Saving user: " + user.getUsername()); } } // persistence/com/example/persistence/uuid/UuidGenerator.java package com.example.persistence.uuid; import com.example.domain.spi.IdGenerator; import java.util.UUID; public class UuidGenerator implements IdGenerator { @Override public String generate() { return UUID.randomUUID().toString(); } } Module 3: Application Layer
// application/module-info.java module com.example.application { requires com.example.domain; requires com.example.persistence; requires java.logging; uses com.example.domain.repository.UserRepository; uses com.example.domain.spi.IdGenerator; exports com.example.application.service; } // application/com/example/application/service/UserService.java package com.example.application.service; import com.example.domain.model.User; import com.example.domain.repository.UserRepository; import com.example.domain.spi.IdGenerator; import java.util.ServiceLoader; import java.util.logging.Logger; public class UserService { private static final Logger LOG = Logger.getLogger(UserService.class.getName()); private final UserRepository userRepository; private final IdGenerator idGenerator; public UserService() { // Load services using ServiceLoader this.userRepository = ServiceLoader.load(UserRepository.class) .findFirst() .orElseThrow(() -> new IllegalStateException("No UserRepository implementation found")); this.idGenerator = ServiceLoader.load(IdGenerator.class) .findFirst() .orElseThrow(() -> new IllegalStateException("No IdGenerator implementation found")); } public void registerUser(String username, String email) { String userId = idGenerator.generate(); User user = new User(username, email); userRepository.save(user); LOG.info("Registered user: " + username + " with ID: " + userId); } public User findUser(String username) { return userRepository.findByUsername(username) .orElseThrow(() -> new IllegalArgumentException("User not found: " + username)); } } 5. Migration Strategies
Automatic Modules (Migration Path)
// When migrating legacy JARs without module-info.java // They become automatic modules module com.example.migrating.app { // Legacy JARs become automatic modules requires commons.lang; // From commons-lang-3.12.0.jar requires guava; // From guava-31.0.jar requires log4j; // From log4j-core-2.17.1.jar requires java.sql; } Example: Migrating Spring Application
// module-info.java for Spring Boot application open module com.example.springapp { requires spring.boot; requires spring.boot.autoconfigure; requires spring.context; requires spring.web; requires spring.data.commons; requires java.persistence; requires java.sql; requires java.validation; // Open for Spring reflection opens com.example.springapp to spring.core, spring.beans, spring.context; opens com.example.springapp.controllers to spring.web; opens com.example.springapp.entities to spring.core, hibernate; opens com.example.springapp.repositories to spring.data.commons; exports com.example.springapp.controllers; exports com.example.springapp.services; } 6. Building and Running Modules
Command Line Compilation
# Compile multiple modules javac -d out \ --module-source-path src \ --module com.example.domain,com.example.persistence,com.example.application # Or compile individually javac -d out/domain \ src/com.example.domain/module-info.java \ src/com.example.domain/com/example/domain/**/*.java javac -d out/persistence \ --module-path out \ --module-source-path src \ --module com.example.persistence
Packaging Modules
# Create modular JARs jar --create --file lib/domain.jar -C out/domain . jar --create --file lib/persistence.jar -C out/persistence . jar --create --file lib/application.jar -C out/application . # Run the application java --module-path lib:mods -m com.example.application/com.example.application.Main
Using jlink to Create Custom Runtime
# Create custom runtime image jlink --module-path $JAVA_HOME/jmods:lib \ --add-modules com.example.application \ --launcher myapp=com.example.application/com.example.application.Main \ --output myruntime # Run from custom runtime myruntime/bin/myapp
7. Advanced Module Features
Module Resolution and Layers
// Creating custom module layers public class ModuleLayerExample { public static void main(String[] args) throws Exception { ModuleFinder finder = ModuleFinder.of(Paths.get("plugins")); ModuleLayer parent = ModuleLayer.boot(); Configuration config = parent.configuration() .resolve(finder, ModuleFinder.of(), Set.of("com.example.plugin")); ClassLoader scl = ClassLoader.getSystemClassLoader(); ModuleLayer layer = parent.defineModulesWithOneLoader(config, scl); // Use plugin Class<?> pluginClass = layer.findLoader("com.example.plugin") .loadClass("com.example.plugin.MyPlugin"); } } Qualified Exports and Opens
module com.example.secure { // Fine-grained access control exports com.example.secure.api to com.example.trusted; opens com.example.secure.internal to com.example.trusted, spring.core; // Conditional exports based on module names } Service Loader in Modules
// Advanced service loading with modules module com.example.plugin.host { uses com.example.spi.Plugin; // Dynamically load plugins public class PluginManager { public List<Plugin> loadPlugins() { return ServiceLoader.load(Plugin.class) .stream() .map(Provider::get) .collect(Collectors.toList()); } } } 8. Testing with Modules
Test Module Structure
// src/test.modules/com.example.test/module-info.java open module com.example.test { requires com.example.application; requires org.junit.jupiter; requires org.mockito; opens com.example.application.test to org.junit.platform.commons; // Read reflection for testing opens com.example.application.service to org.mockito; } JUnit 5 Test in Module
package com.example.application.test; import com.example.application.service.UserService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; import static org.junit.jupiter.api.Assertions.*; class UserServiceTest { private UserService userService; @BeforeEach void setUp() { userService = new UserService(); } @Test void testUserRegistration() { assertDoesNotThrow(() -> { userService.registerUser("testuser", "[email protected]"); }); } } 9. Common Patterns and Best Practices
1. Module Naming
// Use reverse DNS naming module com.company.product.feature { // Avoid: module feature, module mymodule } 2. Layer Separation
// Clear separation of concerns module com.example.app.domain { } module com.example.app.persistence { } module com.example.app.service { } module com.example.app.web { } 3. API Module Pattern
// api/module-info.java module com.example.library.api { exports com.example.library.api; exports com.example.library.spi; } // implementation/module-info.java module com.example.library.impl { requires transitive com.example.library.api; provides com.example.library.spi.Service with com.example.library.impl.ServiceImpl; } 4. Optional Dependencies
module com.example.configurable { requires static com.example.optional.feature; public class FeatureManager { public void useFeature() { // Check if optional module is available boolean hasFeature = ModuleLayer.boot() .findModule("com.example.optional.feature") .isPresent(); if (hasFeature) { // Use optional feature } } } } 10. Troubleshooting Common Issues
Module Resolution Problems
// Common error: module not found module com.example.app { requires non.existent.module; // Error: module not found } // Solution: Check module names and module path Split Packages Issue
// Error: Split package between modules // Module A: exports com.example.util // Module B: exports com.example.util // Solution: Refactor packages or use jdeps to analyze
Reflection Access Denied
// Error: Unable to make field accessible module com.example.reflective { // Solution: Add opens directive opens com.example.reflective.entities; } 11. Tooling Support
jdeps - Dependency Analysis
# Analyze dependencies jdeps -s --module-path lib application.jar # Check for unused dependencies jdeps --api-only --module-path lib application.jar # Generate module-info.java for migration jdeps --generate-module-info ./out legacy.jar
jmod - Module Packaging
# Create JMOD files jmod create --class-path out/module.jar module.jmod # List JMOD contents jmod list module.jmod
jlink - Custom Runtime
# Create minimal runtime jlink --module-path $JAVA_HOME/jmods:mods \ --add-modules java.base,java.logging,com.example.app \ --compress=2 \ --no-header-files \ --no-man-pages \ --output minimal-runtime
12. Real-World Example: Microservice Module
Microservice Module Structure
// module-info.java for microservice open module com.example.userservice { requires spring.boot; requires spring.boot.autoconfigure; requires spring.web; requires spring.data.jpa; requires spring.security.core; requires java.persistence; requires java.validation; requires java.sql; requires com.fasterxml.jackson.databind; requires transitive com.example.common; // Spring reflection access opens com.example.userservice to spring.core, spring.beans, spring.context; opens com.example.userservice.controllers to spring.web, spring.security; opens com.example.userservice.entities to spring.core, hibernate; opens com.example.userservice.repositories to spring.data.commons; opens com.example.userservice.security to spring.security; exports com.example.userservice.controllers; exports com.example.userservice.services; exports com.example.userservice.dto; } Summary
The Java Module System provides:
- Strong encapsulation through explicit exports
- Reliable dependencies with requires directives
- Service abstraction with uses/provides
- Reflection control with opens directives
- Better tooling with jdeps, jlink, jmod
Migration path:
- Start with automatic modules
- Add module-info.java to libraries
- Use jdeps for dependency analysis
- Create custom runtimes with jlink
JPMS enables building more secure, maintainable, and performant Java applications while maintaining backward compatibility with existing code.