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.