10.4 Module Types, Migration Strategies, ServiceLoader SPI, and JLink
Key Takeaways
- The Java Platform Module System classifies modules into three distinct categories: explicit named modules, automatic modules (legacy JARs placed on the module-path), and the unnamed module (code loaded from the classpath).
- Automatic modules derive their module name from the Automatic-Module-Name manifest attribute or their sanitized JAR filename, exporting and opening all packages and reading all other modules.
- The unnamed module represents all classes loaded via the classpath, exports and opens all packages, and can read all loaded modules, but explicit named modules are strictly prohibited from declaring dependencies on the unnamed module.
- The ServiceLoader SPI pattern enables modular decoupling by allowing consumer modules to discover service implementations via uses directives and service providers to register concrete implementations via provides with without exporting implementation packages.
- The jlink tool creates custom, standalone, minimal Java runtime images from modular JARs, but strictly requires all modules in the dependency graph to be explicit named modules, rejecting automatic modules.
Module Types, Migration Strategies, ServiceLoader SPI, and JLink
Migrating legacy enterprise Java applications to the Java Platform Module System requires understanding how the JVM bridges modular code and non-modular code. Java SE 21 categorizes modules into three distinct types: explicit named modules, automatic modules, and the unnamed module. Additionally, JPMS provides native support for loosely coupled plugin architectures via the Service Provider Interface (SPI), java.util.ServiceLoader, and minimal runtime distribution using jlink.
1. The Three Module Categories
+-------------------------------------------------------------------------+
| 1. Explicit Named Module |
| - Placed on: --module-path (-p) |
| - Contains: module-info.class at root |
| - Name: Explicitly declared in module-info.java |
| - Exports/Opens: ONLY packages explicitly declared in descriptor |
| - Reads: ONLY modules explicitly declared via 'requires' |
+-------------------------------------------------------------------------+
^
|
+-------------------------------------------------------------------------+
| 2. Automatic Module |
| - Placed on: --module-path (-p) |
| - Contains: Traditional JAR WITHOUT module-info.class |
| - Name: From Automatic-Module-Name manifest entry OR JAR filename |
| - Exports: ALL packages to all modules |
| - Opens: ALL packages for deep reflection to all modules |
| - Reads: ALL other modules (Named, Automatic, and Unnamed) |
+-------------------------------------------------------------------------+
^
|
+-------------------------------------------------------------------------+
| 3. The Unnamed Module |
| - Placed on: --class-path (-cp) |
| - Contains: All classes/JARs loaded from classpath |
| - Name: None (cannot be referenced by name in module-info.java) |
| - Exports: ALL packages to all modules |
| - Opens: ALL packages for deep reflection |
| - Reads: ALL loaded modules |
+-------------------------------------------------------------------------+
Automatic Module Naming Algorithm
When a non-modular JAR is placed on the --module-path, the JVM automatically synthesizes an in-memory module descriptor using these strict sequential steps:
- Manifest Inspection:
- Checks
META-INF/MANIFEST.MFfor the attributeAutomatic-Module-Name. - If present (e.g.,
Automatic-Module-Name: com.google.guava), that exact string becomes the module name.
- Checks
- Filename Sanitization (Fallback if no manifest entry exists):
- Step A: Strip the file extension
.jar(e.g.,commons-lang3-3.12.0.jar->commons-lang3-3.12.0). - Step B: Remove version numbers and trailing release identifiers matching
-\d.*(e.g.,commons-lang3-3.12.0->commons-lang3). - Step C: Replace all non-alphanumeric characters (hyphens
-, plus signs+) with dots.(e.g.,commons-lang3->commons.lang3). - Step D: Replace consecutive dots with a single dot, and trim any leading or trailing dots.
- Step A: Strip the file extension
JAR Filename Examples -> Automatic Module Name Resolution:
- mysql-connector-j-8.2.0.jar -> mysql.connector.j
- spring-web-6.1.1.jar -> spring.web
- jackson-annotations-2.16.0.jar -> jackson.annotations
- simple-util-1.0-SNAPSHOT.jar -> simple.util
The Unnamed Module Rules & Traps
- Classpath Integration: All JARs and
.classfiles on the classpath belong to a single, shared unnamed module. - Readability: The unnamed module can read all packages exported by all platform modules and all modules on the module path.
- Encapsulation: The unnamed module exports and opens all of its packages to all other modules.
- The One-Way Wall (Critical Exam Rule): Explicit named modules CANNOT require or read the unnamed module! Attempting to write
requires unnamed;or referencing classpath classes from a named module causes a compile-time error.
2. Module Classification Comparison Matrix
| Feature / Behavior | Explicit Named Module | Automatic Module | Unnamed Module |
|---|---|---|---|
| Path Location | --module-path (-p) | --module-path (-p) | --class-path (-cp) |
module-info.class | Present | Absent | Absent |
| Module Name Source | module-info.java | Manifest / JAR Filename | None (Unnamed) |
| Packages Exported | Only explicitly declared | All packages | All packages |
| Packages Opened | Only explicitly declared | All packages | All packages |
| Readability Scope | Declared requires only | All modules (including Unnamed) | All modules |
| Can be required by Named Modules? | Yes (by declared name) | Yes (by automatic name) | No (Strictly forbidden) |
Usable with jlink? | Yes | No | No |
3. Migration Strategies: Bottom-Up vs. Top-Down
Migrating legacy applications to JPMS typically follows one of two primary strategies:
Bottom-Up Migration
- Strategy: Modularize the lowest-level libraries (leaf dependencies) first, moving up the dependency tree until the main application is modularized last.
- Workflow:
- Add
module-info.javato leaf utility JARs and place them on the--module-path. - Higher-level libraries and the main application remain on the
--class-path(unnamed module). - Because the unnamed module can read all modules on the module path, the application runs seamlessly.
- Progressively convert intermediate libraries into explicit named modules.
- Add
- Benefit: Clean, incremental encapsulation without relying on automatic module naming heuristics.
Top-Down Migration
- Strategy: Modularize the main application first, while third-party dependencies remain non-modular.
- Workflow:
- Create
module-info.javafor the main application and place it on--module-path. - Place all third-party legacy JARs without
module-info.classon the--module-path, transforming them into automatic modules. - Declare
requires <automatic-module-name>;inside the application'smodule-info.java.
- Create
- Benefit: The main application immediately gains strong encapsulation and reliable configuration even if third-party libraries have not yet migrated to JPMS.
4. The ServiceLoader API & SPI Architecture
The Service Provider Interface (SPI) pattern enables loose coupling between service consumers and concrete service implementations.
The Three SPI Participants
+--------------------------+
| Service API | <-- Module: com.crypto.api
| interface CipherService | exports com.crypto.api;
+--------------------------+
^ ^
| | (implements)
(uses) | |
+---------------+ +----------------------------------------------+
| Consumer | | Provider Module |
| Module | | provides CipherService with AesCipherImpl; |
+---------------+ +----------------------------------------------+
Step 1: Define the Service Interface (API Module)
// Module: com.example.crypto.api
module com.example.crypto.api {
exports com.example.crypto.api;
}
// Interface in com.example.crypto.api package
package com.example.crypto.api;
public interface CipherService {
String encrypt(String plainText);
String decrypt(String cipherText);
}
Step 2: Implement the Service (Provider Module)
// Concrete Provider Class
package com.example.crypto.aes;
import com.example.crypto.api.CipherService;
public class AesCipher implements CipherService {
public AesCipher() { /* Public no-arg constructor */ }
@Override
public String encrypt(String text) { return "[AES-Encrypted:" + text + "]"; }
@Override
public String decrypt(String cipher) { return cipher.replace("[AES-Encrypted:", "").replace("]", ""); }
}
// Module Descriptor: com.example.crypto.aes
module com.example.crypto.aes {
requires com.example.crypto.api;
provides com.example.crypto.api.CipherService
with com.example.crypto.aes.AesCipher;
}
Provider Construction Rules
A provider class must satisfy at least one of the following requirements:
- Have a
publicno-argument constructor. - Declare a
public staticmethod namedprovider()that takes no arguments and returns the service interface type or a subtype:
public class FastCipher implements CipherService {
private FastCipher() {} // Private constructor
// Factory method recognized by ServiceLoader
public static CipherService provider() {
return new FastCipher();
}
}
[!NOTE] The provider class package does not need to be exported in an
exportsdirective.ServiceLoadercan instantiate the provider class even if its package is completely encapsulated.
Step 3: Consume the Service via ServiceLoader
// Module Descriptor: com.example.crypto.client
module com.example.crypto.client {
requires com.example.crypto.api;
uses com.example.crypto.api.CipherService; // MUST declare uses directive
}
package com.example.crypto.client;
import com.example.crypto.api.CipherService;
import java.util.ServiceLoader;
public class CryptoClient {
public static void main(String[] args) {
// Load all discovered providers
ServiceLoader<CipherService> loader = ServiceLoader.load(CipherService.class);
// Approach A: Iterate and execute all providers
for (CipherService service : loader) {
System.out.println(service.encrypt("Secret Data"));
}
// Approach B: Stream API with lazy provider inspection (Java 9+)
loader.stream()
.filter(p -> p.type().getName().contains("Aes"))
.map(ServiceLoader.Provider::get) // Instantiates on demand
.findFirst()
.ifPresent(s -> System.out.println("Found AES: " + s.encrypt("Payload")));
}
}
5. Creating Custom Runtime Images with jlink
The jlink tool links a set of modules and their transitive dependencies to create a custom, self-contained Java runtime image (a minimal JRE) containing only the modules necessary to run the specific application.
jlink Command Syntax
jlink --module-path "$JAVA_HOME/jmods:mlib" \
--add-modules com.app.client \
--launcher runapp=com.app.client/com.app.client.Main \
--output custom-runtime \
--strip-debug \
--compress 2 \
--no-header-files \
--no-man-pages
Key jlink Flags Explained
--module-path: Must point to the JDK's packaged JMOD files ($JAVA_HOME/jmods) and the directory containing application modular JARs (mlib).--add-modules: Root module(s) to include.jlinkautomatically resolves and includes all transitive dependencies.--launcher <name>=<module>/<mainclass>: Creates an executable binary script incustom-runtime/bin/<name>.--output <dir>: Destination folder for the generated runtime.--strip-debug&--compress: Strips debug symbols and applies bytecode compression to reduce footprint (e.g., from 300+ MB down to 30 MB).
Executing the Custom Runtime
# Run via generated launcher script:
./custom-runtime/bin/runapp
# Or run via the bundled minimal java binary:
./custom-runtime/bin/java -m com.app.client
[!WARNING] Critical Exam Rule for
jlink:jlinkrequires all modules in the dependency graph to be explicit named modules (containing a compiledmodule-info.class).jlinkfails with an error if any dependency is an automatic module (a legacy JAR without a descriptor on the module path) or an unnamed module on the classpath!
A legacy non-modular JAR archive named mysql-connector-java-8.0.28.jar that does not contain a module-info.class and does not define an Automatic-Module-Name manifest header is placed on the --module-path. According to JPMS automatic module naming rules, what is the resulting module name?
An architecture team is migrating an enterprise Java system to JPMS. They modularize the root application first by adding a module-info.java file and place third-party non-modular JAR dependencies on the --module-path, referencing them by automatic module names. What migration strategy is being utilized?
A service provider module declares provides com.app.spi.CacheService with com.app.cache.RedisCache;. If the RedisCache class does not declare a public no-argument constructor, what must it provide to be instantiated by ServiceLoader?
Which statement accurately describes the interaction rules between explicit named modules and the unnamed module in Java SE 21?