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