Java Module System (JPMS) Introduction

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:

  1. Start with automatic modules
  2. Add module-info.java to libraries
  3. Use jdeps for dependency analysis
  4. Create custom runtimes with jlink

JPMS enables building more secure, maintainable, and performant Java applications while maintaining backward compatibility with existing code.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper