google-site-verification: google61fe8ba583a51912.html
Layered Modules and Runtime Images in Java

The Java Module System introduced powerful features for creating layered module architectures and custom runtime images. These capabilities enable better application isolation, dynamic loading, and optimized deployments.

1. Module Layers Overview

What are Module Layers?

  • Module Layers are hierarchical arrangements of modules
  • Each layer has its own module graph and class loaders
  • Enables multiple versions of same module to coexist
  • Supports dynamic module loading and plugin architectures

Key Concepts

  • Boot Layer - The initial layer containing application modules
  • Custom Layers - Additional layers for plugins/extensions
  • Parent-Child Relationship - Layers can have parent layers
  • Isolation - Each layer has separate module resolution

2. Creating Custom Module Layers

Basic Layer Creation

import java.lang.module.*; import java.nio.file.*; import java.util.*; public class BasicLayerExample { public static void main(String[] args) throws Exception { // Step 1: Create module finders ModuleFinder pluginFinder = ModuleFinder.of(Paths.get("plugins")); ModuleFinder appFinder = ModuleFinder.of(Paths.get("app")); // Step 2: Get boot layer configuration ModuleLayer bootLayer = ModuleLayer.boot(); Configuration bootConfig = bootLayer.configuration(); // Step 3: Resolve plugin modules Configuration pluginConfig = bootConfig.resolve( pluginFinder, ModuleFinder.of(), Set.of("com.example.plugin") ); // Step 4: Create class loader and module layer ClassLoader parentLoader = ClassLoader.getSystemClassLoader(); ModuleLayer pluginLayer = bootLayer.defineModulesWithOneLoader( pluginConfig, parentLoader ); System.out.println("Plugin layer created with modules: " + pluginLayer.modules().stream() .map(Module::getName) .collect(Collectors.joining(", "))); } }

Advanced Multi-Layer Architecture

import java.lang.module.*; import java.nio.file.*; import java.util.*; import java.util.stream.*; public class AdvancedLayerManager { private final Map<String, ModuleLayer> layers = new HashMap<>(); private final ModuleLayer bootLayer; public AdvancedLayerManager() { this.bootLayer = ModuleLayer.boot(); } /** * Creates a new module layer for plugins */ public ModuleLayer createPluginLayer(String layerName, Path pluginPath, Set<String> rootModules) throws Exception { ModuleFinder pluginFinder = ModuleFinder.of(pluginPath); Configuration parentConfig = bootLayer.configuration(); Configuration pluginConfig = parentConfig.resolve( pluginFinder, ModuleFinder.of(), rootModules ); ClassLoader layerLoader = new LayerClassLoader("layer-" + layerName); ModuleLayer pluginLayer = bootLayer.defineModulesWithOneLoader( pluginConfig, layerLoader ); layers.put(layerName, pluginLayer); return pluginLayer; } /** * Creates a child layer with specific parent layers */ public ModuleLayer createChildLayer(String layerName, Path modulePath, Set<String> rootModules, ModuleLayer... parents) throws Exception { List<Configuration> parentConfigs = Arrays.stream(parents) .map(ModuleLayer::configuration) .collect(Collectors.toList()); ModuleFinder finder = ModuleFinder.of(modulePath); Configuration config = Configuration.resolve( finder, parentConfigs, ModuleFinder.of(), rootModules ); ClassLoader layerLoader = new LayerClassLoader("layer-" + layerName); List<ModuleLayer> parentLayers = Arrays.asList(parents); ModuleLayer childLayer = ModuleLayer.defineModulesWithOneLoader( config, parentLayers, layerLoader ); layers.put(layerName, childLayer); return childLayer; } /** * Finds a class in all layers */ public Optional<Class<?>> findClass(String className) { for (ModuleLayer layer : layers.values()) { try { ClassLoader loader = layer.findLoader(className); if (loader != null) { return Optional.of(Class.forName(className, true, loader)); } } catch (ClassNotFoundException e) { // Continue searching in other layers } } return Optional.empty(); } /** * Gets all modules across all layers */ public Set<Module> getAllModules() { return layers.values().stream() .flatMap(layer -> layer.modules().stream()) .collect(Collectors.toSet()); } // Custom class loader for layer isolation static class LayerClassLoader extends ClassLoader { private final String layerName; public LayerClassLoader(String layerName) { this.layerName = layerName; } @Override public String toString() { return "LayerClassLoader[" + layerName + "]"; } } }

3. Complete Plugin System Example

Plugin Architecture with Module Layers

Core Application Module

// core-app/module-info.java module com.example.core { exports com.example.core.api; exports com.example.core.spi; uses com.example.core.spi.Plugin; }
// core-app/com/example/core/api/PluginContext.java package com.example.core.api; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class PluginContext { private final Map<String, Object> attributes = new ConcurrentHashMap<>(); public void setAttribute(String key, Object value) { attributes.put(key, value); } @SuppressWarnings("unchecked") public <T> T getAttribute(String key) { return (T) attributes.get(key); } public <T> T getAttribute(String key, Class<T> type) { Object value = attributes.get(key); return type.isInstance(value) ? type.cast(value) : null; } }
// core-app/com/example/core/spi/Plugin.java package com.example.core.spi; import com.example.core.api.PluginContext; public interface Plugin { String getName(); String getVersion(); void initialize(PluginContext context); void start(); void stop(); boolean isRunning(); }

Plugin Manager with Module Layers

// core-app/com/example/core/PluginManager.java package com.example.core; import com.example.core.api.PluginContext; import com.example.core.spi.Plugin; import java.lang.module.*; import java.nio.file.*; import java.util.*; import java.util.concurrent.*; import java.util.stream.*; public class PluginManager { private final Map<String, ModuleLayer> pluginLayers = new ConcurrentHashMap<>(); private final Map<String, Plugin> plugins = new ConcurrentHashMap<>(); private final PluginContext context; public PluginManager(PluginContext context) { this.context = context; } /** * Loads a plugin from the specified directory */ public void loadPlugin(String pluginId, Path pluginDir) throws Exception { if (pluginLayers.containsKey(pluginId)) { throw new IllegalStateException("Plugin already loaded: " + pluginId); } ModuleFinder pluginFinder = ModuleFinder.of(pluginDir); ModuleLayer bootLayer = ModuleLayer.boot(); // Find all modules in plugin directory Set<String> pluginModules = pluginFinder.findAll().stream() .map(ModuleReference::descriptor) .map(ModuleDescriptor::name) .collect(Collectors.toSet()); if (pluginModules.isEmpty()) { throw new IllegalArgumentException("No modules found in: " + pluginDir); } // Resolve plugin configuration Configuration pluginConfig = bootLayer.configuration().resolve( pluginFinder, ModuleFinder.of(), pluginModules ); // Create isolated class loader for plugin ClassLoader pluginLoader = new PluginClassLoader(pluginId); ModuleLayer pluginLayer = bootLayer.defineModulesWithOneLoader( pluginConfig, pluginLoader ); pluginLayers.put(pluginId, pluginLayer); // Discover and initialize plugins initializePluginsInLayer(pluginId, pluginLayer); } private void initializePluginsInLayer(String pluginId, ModuleLayer layer) { ServiceLoader<Plugin> loader = ServiceLoader.load(layer, Plugin.class); for (Plugin plugin : loader) { String pluginName = plugin.getName(); System.out.println("Initializing plugin: " + pluginName + " from layer: " + pluginId); try { plugin.initialize(context); plugins.put(pluginName, plugin); } catch (Exception e) { System.err.println("Failed to initialize plugin: " + pluginName); e.printStackTrace(); } } } /** * Starts all loaded plugins */ public void startAllPlugins() { plugins.values().forEach(plugin -> { try { if (!plugin.isRunning()) { plugin.start(); System.out.println("Started plugin: " + plugin.getName()); } } catch (Exception e) { System.err.println("Failed to start plugin: " + plugin.getName()); e.printStackTrace(); } }); } /** * Stops and unloads a plugin */ public void unloadPlugin(String pluginId) { ModuleLayer layer = pluginLayers.get(pluginId); if (layer == null) { return; } // Stop all plugins from this layer plugins.entrySet().removeIf(entry -> { Plugin plugin = entry.getValue(); if (plugin.getClass().getModule().getLayer() == layer) { try { plugin.stop(); System.out.println("Stopped plugin: " + plugin.getName()); } catch (Exception e) { System.err.println("Error stopping plugin: " + plugin.getName()); } return true; } return false; }); pluginLayers.remove(pluginId); // Note: In real implementation, you might need to handle // resource cleanup and class loader unloading more carefully } /** * Gets all loaded plugins */ public List<Plugin> getPlugins() { return new ArrayList<>(plugins.values()); } /** * Finds plugin by name */ public Optional<Plugin> getPlugin(String name) { return Optional.ofNullable(plugins.get(name)); } // Custom class loader for plugin isolation static class PluginClassLoader extends ClassLoader { private final String pluginId; public PluginClassLoader(String pluginId) { this.pluginId = pluginId; } @Override public String toString() { return "PluginClassLoader[" + pluginId + "]"; } } }

Example Plugin Implementation

// calculator-plugin/module-info.java module com.example.calculator.plugin { requires com.example.core; provides com.example.core.spi.Plugin with com.example.calculator.plugin.CalculatorPlugin; exports com.example.calculator.plugin; }
// calculator-plugin/com/example/calculator/plugin/CalculatorPlugin.java package com.example.calculator.plugin; import com.example.core.api.PluginContext; import com.example.core.spi.Plugin; public class CalculatorPlugin implements Plugin { private volatile boolean running = false; private PluginContext context; @Override public String getName() { return "Calculator Plugin"; } @Override public String getVersion() { return "1.0.0"; } @Override public void initialize(PluginContext context) { this.context = context; System.out.println("Calculator Plugin initialized"); // Register calculator service in context context.setAttribute("calculator", new CalculatorService()); } @Override public void start() { running = true; System.out.println("Calculator Plugin started"); } @Override public void stop() { running = false; System.out.println("Calculator Plugin stopped"); } @Override public boolean isRunning() { return running; } // Plugin-specific functionality public static class CalculatorService { public double add(double a, double b) { return a + b; } public double subtract(double a, double b) { return a - b; } public double multiply(double a, double b) { return a * b; } public double divide(double a, double b) { return a / b; } } }

Main Application

// core-app/com/example/core/MainApplication.java package com.example.core; import com.example.core.api.PluginContext; import java.nio.file.*; public class MainApplication { public static void main(String[] args) throws Exception { PluginContext context = new PluginContext(); PluginManager pluginManager = new PluginManager(context); // Load plugins from different directories Path pluginsDir = Paths.get("plugins"); if (Files.exists(pluginsDir)) { Files.list(pluginsDir) .filter(Files::isDirectory) .forEach(pluginDir -> { try { String pluginId = pluginDir.getFileName().toString(); pluginManager.loadPlugin(pluginId, pluginDir); } catch (Exception e) { System.err.println("Failed to load plugin from: " + pluginDir); e.printStackTrace(); } }); } // Start all plugins pluginManager.startAllPlugins(); // Demonstrate plugin functionality pluginManager.getPlugins().forEach(plugin -> { System.out.println("Active plugin: " + plugin.getName() + " (Running: " + plugin.isRunning() + ")"); }); // Keep application running System.out.println("Application running. Press Ctrl+C to exit."); Thread.sleep(Long.MAX_VALUE); } }

4. Custom Runtime Images with jlink

Creating Minimal Runtime Images

Basic jlink Usage

# Create a custom runtime image jlink --module-path $JAVA_HOME/jmods:mods \ --add-modules java.base,java.logging,com.example.app \ --output custom-runtime # Create with compression and stripped debug info jlink --module-path $JAVA_HOME/jmods:mods \ --add-modules java.base,java.sql,java.logging,com.example.app \ --compress=2 \ --no-header-files \ --no-man-pages \ --strip-debug \ --output minimal-runtime

Advanced jlink Configuration

# Create runtime with specific VM options jlink --module-path $JAVA_HOME/jmods:mods \ --add-modules java.base,java.logging,java.management,com.example.app \ --output production-runtime \ --launcher start-app=com.example.app/com.example.app.Main \ --vm=server \ --bind-services \ --ignore-signing-information

jlink Plugin System

// Example: Custom jlink plugin for resource optimization import jdk.tools.jlink.plugin.*; import jdk.tools.jlink.builder.*; import java.util.*; public class ResourceOptimizerPlugin implements Plugin { private final Set<String> excludedResources = Set.of( "*.md", "*.txt", "*.html", "META-INF/LICENSE" ); @Override public String getName() { return "resource-optimizer"; } @Override public String getDescription() { return "Removes unnecessary resources from runtime image"; } @Override public Category getType() { return Category.FILTER; } @Override public boolean hasArguments() { return true; } @Override public String getArgumentsDescription() { return "<exclude-patterns> - Comma separated list of patterns to exclude"; } @Override public void configure(Map<String, String> config) { String patterns = config.get("exclude-patterns"); if (patterns != null) { excludedResources.clear(); Collections.addAll(excludedResources, patterns.split(",")); } } @Override public ResourcePool transform(ResourcePool in, ResourcePoolBuilder out) { in.transformAndCopy((resource) -> { String path = resource.path(); // Check if resource should be excluded for (String pattern : excludedResources) { if (matchesPattern(path, pattern)) { System.out.println("Excluding resource: " + path); return null; // Exclude this resource } } return resource; }, out); return out.build(); } private boolean matchesPattern(String path, String pattern) { if (pattern.startsWith("*.")) { String extension = pattern.substring(1); return path.endsWith(extension); } return path.contains(pattern); } }

Building Custom Runtime with jlink API

import jdk.tools.jlink.*; import jdk.tools.jlink.plugin.*; import java.nio.file.*; import java.util.*; public class RuntimeImageBuilder { public Path buildCustomRuntime(Path modulePath, Set<String> modules, Path outputDir) throws Exception { // Create jlink configuration JlinkTask task = JlinkTask.builder() .modulePath(modulePath) .addModules(modules) .output(outputDir) .endianness(JlinkTask.Endianness.NATIVE) .compress(JlinkTask.Compression.ZIP) .build(); // Execute jlink task task.call(); return outputDir.resolve("bin").resolve("java"); } public Path buildOptimizedRuntime(Path modulePath, Set<String> modules, Path outputDir, List<Plugin> plugins) throws Exception { JlinkTask.Builder builder = JlinkTask.builder() .modulePath(modulePath) .addModules(modules) .output(outputDir) .compress(JlinkTask.Compression.ZIP) .stripDebug(true) .noHeaderFiles(true) .noManPages(true); // Add custom plugins for (Plugin plugin : plugins) { builder.plugin(plugin); } return builder.build().call(); } }

5. Dynamic Module Loading at Runtime

Runtime Module Resolution

import java.lang.module.*; import java.nio.file.*; import java.util.*; import java.util.stream.*; public class DynamicModuleLoader { private final Map<String, ResolvedModule> loadedModules = new HashMap<>(); private final ModuleLayer bootLayer; public DynamicModuleLoader() { this.bootLayer = ModuleLayer.boot(); } /** * Dynamically loads a module at runtime */ public Module loadModule(Path modulePath, String moduleName) throws Exception { ModuleFinder finder = ModuleFinder.of(modulePath); Optional<ModuleReference> moduleRef = finder.find(moduleName); if (moduleRef.isEmpty()) { throw new IllegalArgumentException("Module not found: " + moduleName); } Configuration parentConfig = bootLayer.configuration(); Configuration newConfig = parentConfig.resolveAndBind( finder, ModuleFinder.of(), Set.of(moduleName) ); ClassLoader loader = new DynamicModuleClassLoader(moduleName); ModuleLayer newLayer = bootLayer.defineModulesWithOneLoader(newConfig, loader); Optional<Module> module = newLayer.findModule(moduleName); if (module.isPresent()) { loadedModules.put(moduleName, newConfig.findModule(moduleName).get()); return module.get(); } throw new IllegalStateException("Failed to load module: " + moduleName); } /** * Unloads a dynamically loaded module * Note: This is simplified - real unloading is complex */ public void unloadModule(String moduleName) { loadedModules.remove(moduleName); // In practice, you'd need to handle class loader cleanup } /** * Gets dependencies of a module */ public Set<String> getDependencies(String moduleName) { ResolvedModule resolved = loadedModules.get(moduleName); if (resolved == null) { return Set.of(); } return resolved.reads().stream() .map(ResolvedModule::name) .collect(Collectors.toSet()); } static class DynamicModuleClassLoader extends ClassLoader { private final String moduleName; public DynamicModuleClassLoader(String moduleName) { this.moduleName = moduleName; } @Override public String toString() { return "DynamicModuleClassLoader[" + moduleName + "]"; } } }

6. Service Binding in Module Layers

Service Binding Across Layers

import java.lang.module.*; import java.util.*; import java.util.stream.*; public class CrossLayerServiceManager { private final List<ModuleLayer> layers = new ArrayList<>(); public CrossLayerServiceManager() { layers.add(ModuleLayer.boot()); } /** * Finds services across all layers */ public <T> List<T> findAllServices(Class<T> serviceClass) { return layers.stream() .flatMap(layer -> ServiceLoader.load(layer, serviceClass).stream()) .map(ServiceLoader.Provider::get) .collect(Collectors.toList()); } /** * Finds service in specific layer */ public <T> Optional<T> findServiceInLayer(Class<T> serviceClass, ModuleLayer layer) { return ServiceLoader.load(layer, serviceClass) .stream() .map(ServiceLoader.Provider::get) .findFirst(); } /** * Creates service bridge between layers */ public <T> void createServiceBridge(Class<T> serviceClass, ModuleLayer sourceLayer, ModuleLayer targetLayer) { // This would require reflection or code generation // to bridge services between isolated layers System.out.println("Creating service bridge for " + serviceClass.getName() + " from " + sourceLayer + " to " + targetLayer); } }

7. Best Practices and Patterns

1. Layer Isolation Strategy

public class LayerIsolationManager { private final Map<String, ModuleLayer> layers = new ConcurrentHashMap<>(); private final Map<String, ClassLoader> loaders = new ConcurrentHashMap<>(); public ModuleLayer createIsolatedLayer(String layerId, Path modulePath, Set<String> rootModules) throws Exception { ModuleFinder finder = ModuleFinder.of(modulePath); ModuleLayer parent = ModuleLayer.boot(); Configuration config = parent.configuration().resolve( finder, ModuleFinder.of(), rootModules ); // Create completely isolated class loader ClassLoader isolatedLoader = new IsolatedClassLoader(layerId); ModuleLayer layer = parent.defineModulesWithOneLoader(config, isolatedLoader); layers.put(layerId, layer); loaders.put(layerId, isolatedLoader); return layer; } public void destroyLayer(String layerId) { ModuleLayer layer = layers.remove(layerId); ClassLoader loader = loaders.remove(layerId); if (loader instanceof IsolatedClassLoader) { ((IsolatedClassLoader) loader).cleanup(); } // Additional cleanup for layer resources System.out.println("Destroyed layer: " + layerId); } static class IsolatedClassLoader extends ClassLoader { private final String layerId; public IsolatedClassLoader(String layerId) { super(null); // No parent - complete isolation this.layerId = layerId; } public void cleanup() { // Clean up resources, close opened resources, etc. } @Override public String toString() { return "IsolatedClassLoader[" + layerId + "]"; } } }

2. Module Version Management

public class ModuleVersionManager { private final Map<String, List<ModuleLayer>> versionedLayers = new HashMap<>(); /** * Loads specific version of a module */ public ModuleLayer loadModuleVersion(String moduleName, String version, Path versionedPath) throws Exception { ModuleFinder finder = ModuleFinder.of(versionedPath); String versionedModuleName = moduleName + ".v" + version.replace('.', '_'); ModuleLayer bootLayer = ModuleLayer.boot(); Configuration config = bootLayer.configuration().resolve( finder, ModuleFinder.of(), Set.of(versionedModuleName) ); ClassLoader versionedLoader = new VersionedClassLoader(moduleName, version); ModuleLayer versionedLayer = bootLayer.defineModulesWithOneLoader( config, versionedLoader ); versionedLayers.computeIfAbsent(moduleName, k -> new ArrayList<>()) .add(versionedLayer); return versionedLayer; } /** * Gets all loaded versions of a module */ public List<String> getLoadedVersions(String moduleName) { return versionedLayers.getOrDefault(moduleName, List.of()).stream() .map(layer -> extractVersionFromLayer(layer)) .collect(Collectors.toList()); } private String extractVersionFromLayer(ModuleLayer layer) { // Extract version from layer or class loader return "unknown"; } static class VersionedClassLoader extends ClassLoader { private final String moduleName; private final String version; public VersionedClassLoader(String moduleName, String version) { this.moduleName = moduleName; this.version = version; } @Override public String toString() { return "VersionedClassLoader[" + moduleName + "-" + version + "]"; } } }

8. Troubleshooting and Diagnostics

Layer Diagnostics Tool

public class LayerDiagnostics { public static void printLayerHierarchy(ModuleLayer layer, String indent) { System.out.println(indent + "Layer: " + layer); System.out.println(indent + " Parent: " + layer.parents()); System.out.println(indent + " Modules: " + layer.modules().stream() .map(Module::getName) .collect(Collectors.joining(", "))); // Recursively print parent layers for (ModuleLayer parent : layer.parents()) { printLayerHierarchy(parent, indent + " "); } } public static void analyzeLayerConfiguration(ModuleLayer layer) { Configuration config = layer.configuration(); System.out.println("Configuration Analysis:"); System.out.println(" Modules: " + config.modules().size()); config.modules().forEach(module -> { System.out.println(" Module: " + module.name()); System.out.println(" Reads: " + module.reads().stream() .map(ResolvedModule::name) .collect(Collectors.joining(", "))); }); } public static void checkLayerIsolation(ModuleLayer layer1, ModuleLayer layer2) { boolean canAccess = layer1.modules().stream() .anyMatch(module1 -> layer2.modules().stream() .anyMatch(module2 -> module1.getPackages().stream() .anyMatch(pkg -> module2.getPackages().contains(pkg)))); System.out.println("Layer isolation: " + (canAccess ? "BROKEN" : "MAINTAINED")); } }

Summary

Layered modules and runtime images provide:

  • Module Isolation - Separate module graphs and class loaders
  • Dynamic Loading - Load modules at runtime
  • Plugin Architectures - Support for extensible applications
  • Optimized Deployment - Custom runtime images with jlink
  • Version Management - Multiple versions of same module

Key benefits:

  • Better application architecture through clear boundaries
  • Improved security through module isolation
  • Smaller deployment footprints with jlink
  • Dynamic extensibility without application restarts

These features enable building modern, modular, and scalable Java applications that can evolve independently and deploy efficiently.

Leave a Reply

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


Macro Nepal Helper