12.4 Services in JPMS (`uses`, `provides...with`), Migration Paths, and Tooling (`jlink`, `jdeps`)
Key Takeaways
JPMS service decoupling relies on three components: the service interface/class, the provider implementation registered via 'provides...with', and the consumer declaring 'uses'.
Service provider implementations require either a public no-argument constructor or a public static provider() factory method, and do not need to reside in an exported package.
ServiceLoader.load(Class) dynamically resolves registered providers, offering stream-based lazy inspection to evaluate provider types prior to instantiation.
Migration strategies leverage the Unnamed Module (for classpath code) and Automatic Modules (for regular JARs on the module-path that export and open all packages and read all modules).
The jdeps tool analyzes class and module dependency graphs, while jlink builds custom, minimal self-contained runtime images from explicit modular JARs, strictly rejecting automatic or unnamed modules.
Services in JPMS (uses, provides...with), Migration Paths, and Tooling (jlink, jdeps)
A core design goal of JPMS is loose coupling between application components. While requires and exports establish direct module-to-module dependencies, JPMS integrates with the java.util.ServiceLoader API to support modular Service-Oriented Architectures (SOA). Furthermore, JPMS provides clear migration pathways for legacy codebases alongside command-line diagnostic and packaging utilities.
The JPMS Service Architecture
The service architecture decouples consumer modules from concrete provider implementations through an intermediary service interface or abstract class. Three architectural roles participate in the service relationship:
- Service Interface / Class: The common contract (e.g.,
PaymentService). Can reside in its own API module. - Service Consumer: A module that uses the service interface without knowing or requiring specific concrete implementation classes.
- Service Provider: A module that implements the service interface and registers its concrete provider class.
+----------------------+ +-------------------------+
| Consumer Module | | Provider Module |
| | | |
| uses PaymentService; | | provides PaymentService |
| | | with StripePaymentImpl; |
+----------+-----------+ +------------+------------+
| |
| reads | implements & reads
v v
+----+-----------------------------------+----+
| Service Module |
| |
| interface PaymentService { ... } |
+---------------------------------------------+
The Consumer Declaration: uses
The consumer module declares that it discovers and consumes implementations of a service interface:
module com.ecommerce.checkout {
requires com.ecommerce.payment.api;
uses com.ecommerce.payment.api.PaymentService;
}
Warning
The Missing uses Directive Trap
If a module invokes ServiceLoader.load(PaymentService.class) without declaring uses PaymentService; in its module-info.java, the call fails. Under JPMS, ServiceLoader checks the calling module's descriptor; if the uses directive is absent, ServiceLoader.load throws a java.util.ServiceConfigurationError stating that the module does not declare uses. It does not quietly return an empty loader.
The Provider Declaration: provides ... with ...
A provider module registers its implementation using the provides directive:
module com.ecommerce.payment.stripe {
requires com.ecommerce.payment.api;
provides com.ecommerce.payment.api.PaymentService
with com.ecommerce.payment.stripe.StripePaymentService;
}
Multiple provider implementations can be registered in a single directive by separating class names with commas:
provides PaymentService with StripePaymentService, StripeDirectDebitService;
Rules for Service Provider Implementation Classes
For a provider class to be valid under JPMS, it must satisfy specific compiler and runtime constraints:
- Type Compatibility: The provider class must implement the service interface or extend the service abstract class (unless it supplies a
provider()method, in which case that method's return type must be compatible with the service). - Public Accessibility: The provider class must be
publicand cannot be an inner or abstract class. - Instantiation Contract: The provider class must offer either:
- A public no-argument constructor, OR
- A public static
provider()factory method that returns an instance of the service interface (or subtype).
- No Export Required: The package containing the implementation class does NOT need to be exported or opened! This is a key advantage of JPMS services: the concrete implementation class remains fully encapsulated inside its module, yet
ServiceLoadercan instantiate it via the authorization granted by theprovidesdeclaration.
Loading Services with ServiceLoader
Consumers discover providers dynamically at runtime:
ServiceLoader<PaymentService> loader = ServiceLoader.load(PaymentService.class);
// 1. Traditional iteration (instantiates all providers eagerly during iteration):
for (PaymentService service : loader) {
service.processPayment(amount);
}
// 2. Stream-based inspection (Java 9+ lazy loading):
loader.stream()
.filter(provider -> provider.type().getSimpleName().startsWith("Stripe"))
.map(ServiceLoader.Provider::get) // Instantiates ONLY matching providers
.findFirst()
.ifPresent(service -> service.processPayment(amount));
Migration Paths: Unnamed Module vs. Automatic Module
Migrating legacy applications to JPMS does not require an all-or-nothing modularization. Java provides two transitional module abstractions:
1. The Unnamed Module (Classpath)
Any JAR file or class loaded from the classpath (-cp) belongs to the Unnamed Module:
- No Name: Has no formal module name (evaluates to
nullviaModule.getName()). - Reads Everything: Automatically reads all named modules, automatic modules, and JDK modules.
- Exports & Opens Everything: All packages in the unnamed module are exported and opened to all other modules at runtime.
- Cannot Be Required: Named modules on the module-path cannot declare
requires <unnamed>;—attempting to do so triggers a compile error. Code in named modules cannot access code on the classpath.
2. Automatic Modules (Module-Path)
When a regular, non-modular JAR (lacking module-info.class) is placed on the module-path (-p), the JVM automatically wraps it as an Automatic Module:
- Naming Rules:
- If
META-INF/MANIFEST.MFdefines theAutomatic-Module-Nameheader (e.g.,Automatic-Module-Name: com.google.gson), that value becomes the module name. - Otherwise, the name is derived from the JAR filename: strip
.jar, remove trailing version numbers (-1.2.3), replace non-alphanumeric characters with dots (.), and remove duplicate or leading/trailing dots (e.g.,commons-lang3-3.12.0.jarbecomescommons.lang3).
- If
- Capabilities:
- Reads all other modules (including the unnamed module and all other automatic modules).
- Exports all packages to all modules.
- Opens all packages for deep reflection to all modules.
- Can be required by named modules (
requires commons.lang3;).
Essential JPMS Diagnostic and Build Tooling
JPMS includes command-line tools to inspect, diagnose, and package modular applications.
Analyzing Dependencies with jdeps
The jdeps command is the Java Dependency Analysis Tool. It scans class files or JARs and reports package-level and module-level dependencies:
# List all module dependencies of an application JAR:
jdeps --module-path lib -s app.jar
# List dependencies omitting transitive dependencies (reduced dependency set):
jdeps --module-path lib --list-reduced-deps app.jar
# Scan for prohibited internal JDK APIs:
jdeps --jdk-internals app.jar
# Automatically generate a starter module-info.java for a legacy JAR:
jdeps --generate-module-info output_dir legacy-library.jar
Assembling Minimal Runtimes with jlink
The jlink command links a set of modular JARs and JDK modules to generate a custom, standalone runtime image containing only the exact modules required to execute the application.
jlink --module-path $JAVA_HOME/jmods:lib \
--add-modules com.ecommerce.checkout \
--output custom-runtime \
--launcher start=com.ecommerce.checkout/com.ecommerce.checkout.Main \
--compress=2 \
--strip-debug
Benefits of jlink:
- Drastic Footprint Reduction: Strips unused JDK modules, reducing the runtime size from over 300 MB down to 30–50 MB.
- Embedded Launcher: Generates executable native binary scripts (
custom-runtime/bin/start). - No JRE Required: The custom image is self-contained and runs on deployment environments without requiring an installed JDK/JRE.
Warning
The jlink Automatic Module Limitation
The jlink tool cannot bundle Automatic Modules or Unnamed Modules. Every module included in a jlink runtime image must be an explicit, fully modular artifact containing a valid module-info.class. Attempting to link an automatic module causes jlink to terminate with an error: Error: automatic module cannot be used with jlink.
A module declares a service provider as follows:
module com.logger.provider {
requires com.logger.api;
provides com.logger.api.Logger with com.logger.provider.FastLogger;
}
Which set of requirements must FastLogger satisfy for this module declaration to compile and load properly via ServiceLoader?
FastLogger must be declared in an exported package and provide a public constructor accepting a String parameter.
FastLogger must be declared as final and reside in an opened package to allow reflective instantiation.
FastLogger must implement java.io.Serializable and be registered in META-INF/services/com.logger.api.Logger.
FastLogger must be a public class implementing com.logger.api.Logger, providing either a public no-arg constructor or a public static provider() method.
A legacy non-modular JAR named commons-math3-3.6.1.jar does not contain module-info.class and does not include an Automatic-Module-Name header in its manifest. If this JAR is placed on the --module-path, what is its automatic module name?
commons.math3.3.6.1
commons_math3
commons-math3
commons.math3
A developer runs the following command to assemble a custom runtime image:
jlink --module-path $JAVA_HOME/jmods:lib --add-modules com.app.main --output runtime
The module com.app.main requires com.google.gson, which is an automatic module derived from gson-2.8.9.jar on the module-path. What is the outcome of running this command?
jlink successfully creates the image and bundles the automatic module as a standard runtime module.
jlink fails with an error indicating that automatic modules cannot be used with jlink.
jlink converts the automatic module into an open module inside the custom runtime directory.
jlink creates the image but displays a runtime warning that reflection will be restricted.
Sections you finish are checked off in the contents.