Skip to Content
Course content

184: Services and Service Loaders in JPMS

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been working on a payment gateway for a client, and I hit a wall today. I have a core module that handles checkouts, but I need to support multiple payment providers—Stripe, PayPal, and maybe some local bank APIs later. My first instinct was to just create a list of implementations and instantiate them manually, but that creates a nightmare of dependencies. I don't want my checkout-core module to have to know every single single provider class that exists in the ecosystem.

The Hard-Coded Wall

I started with a simple interface in my com.payment.api module:

public interface PaymentProcessor {
    void process(double amount);
}

Then, in a separate module com.payment.stripe, I implemented it. Naturally, I tried to call it from my main app like this:

PaymentProcessor processor = new StripeProcessor();
processor.process(100.0);

This works, but it's brittle. If I want to add PayPal, I have to modify the main app's code, recompile, and add another dependency. That's not a plugin system; that's just a hard-coded list. I want the app to say, "Give me whatever payment processors are available on the classpath/module-path," and have Java just figure it out.

The Silent Failure of ServiceLoader

I remember reading about ServiceLoader, so I decided to give it a shot. I deleted the new StripeProcessor() line and replaced it with this:

ServiceLoader<PaymentProcessor> loader = ServiceLoader.load(PaymentProcessor.class);
for (PaymentProcessor p : loader) {
    p.process(100.0);
}

I ran it. Nothing happened. No crash, no exception—just total silence. The loop didn't execute a single time. I checked my imports, I checked the JARs, and everything looked correct. In the pre-module days, I would have just dropped a file into META-INF/services and called it a day, but we're in the JPMS world now. The module system is much stricter about who can see what.

Closing the Gap in module-info

I realized the checkout-core module doesn't actually have permission to "use" the service, and the stripe module hasn't officially "provided" it. The code is there, but the module metadata is missing.

First, I went into the checkout-core module's module-info.java. I need to tell Java that this module intends to consume a service. I added the uses keyword:

module com.payment.core {
    requires com.payment.api;
    uses com.payment.api.PaymentProcessor;
}

Still nothing. I ran it again, and the list was still empty. Right, because the implementation module—com.payment.stripe—is still keeping its implementation class private (as it should be). I need to explicitly export that implementation as a service provider.

I opened the module-info.java for the Stripe module and added the provides...with clause:

module com.payment.stripe {
    requires com.payment.api;
    provides com.payment.api.PaymentProcessor 
        with com.payment.stripe.StripeProcessor;
}

The Payoff

I fired it up one more time. This time, the console lit up: "Processing $100.0 via Stripe". The magic here is that checkout-core still has absolutely no dependency on com.payment.stripe. If I create a new com.payment.paypal module and add it to the module path, the ServiceLoader will pick it up automatically without me changing a single line of code in the core logic.

It's a decoupled architecture. The uses statement in the consumer tells the JVM to look for providers, and the provides...with statement in the provider registers the implementation. It's a clean handshake that keeps my core logic lean and my plugins truly pluggable.




📋 Practical Task

Build a Pluggable Logging Framework

You need to create a mini-plugin system for a logging framework using JPMS. Your goal is to allow the main application to log messages without knowing which logging implementation is being used (Console, File, or Database).

  • Module 1 (com.logger.api): Define an interface LoggerService with a method void log(String message).
  • Module 2 (com.logger.impl.console): Create a class ConsoleLogger that implements LoggerService and prints to System.out. Configure the module-info.java to provide this service.
  • Module 3 (com.logger.app): Create a main class that uses ServiceLoader to find all available LoggerService implementations and calls the log method on each. Configure the module-info.java to use the LoggerService.

Verification: Run the app and verify that "Hello from ConsoleLogger" is printed. Then, create a second implementation module (e.g., com.logger.impl.file) and add it to the module path to see if the app picks up both loggers without needing a recompile of the com.logger.app module.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.