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