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.
Last updated: September 2026

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:

  1. Manifest Inspection:
    • Checks META-INF/MANIFEST.MF for the attribute Automatic-Module-Name.
    • If present (e.g., Automatic-Module-Name: com.google.guava), that exact string becomes the module name.
  2. 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.
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 .class files 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 / BehaviorExplicit Named ModuleAutomatic ModuleUnnamed Module
Path Location--module-path (-p)--module-path (-p)--class-path (-cp)
module-info.classPresentAbsentAbsent
Module Name Sourcemodule-info.javaManifest / JAR FilenameNone (Unnamed)
Packages ExportedOnly explicitly declaredAll packagesAll packages
Packages OpenedOnly explicitly declaredAll packagesAll packages
Readability ScopeDeclared requires onlyAll 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?YesNoNo

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:
    1. Add module-info.java to leaf utility JARs and place them on the --module-path.
    2. Higher-level libraries and the main application remain on the --class-path (unnamed module).
    3. Because the unnamed module can read all modules on the module path, the application runs seamlessly.
    4. Progressively convert intermediate libraries into explicit named modules.
  • 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:
    1. Create module-info.java for the main application and place it on --module-path.
    2. Place all third-party legacy JARs without module-info.class on the --module-path, transforming them into automatic modules.
    3. Declare requires <automatic-module-name>; inside the application's module-info.java.
  • 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:

  1. Have a public no-argument constructor.
  2. Declare a public static method named provider() 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 exports directive. ServiceLoader can 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. jlink automatically resolves and includes all transitive dependencies.
  • --launcher <name>=<module>/<mainclass>: Creates an executable binary script in custom-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: jlink requires all modules in the dependency graph to be explicit named modules (containing a compiled module-info.class). jlink fails 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!

Loading diagram...
Decoupled ServiceLoader Discovery Architecture
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

Which statement accurately describes the interaction rules between explicit named modules and the unnamed module in Java SE 21?

A
B
C
D