Module Descriptors and module-info.java in Java

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:

  1. Strong Encapsulation: Explicit control over exported APIs
  2. Reliable Configuration: Explicit dependency declaration
  3. Improved Security: Reduced attack surface
  4. Better Performance: Startup time and memory footprint improvements
  5. 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 transitive for 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.

Leave a Reply

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


Macro Nepal Helper