39. Services in JPMS (The ServiceLoader Model)
Table of Contents
JPMS includes a built-in service mechanism that allows modules to discover and use implementations at runtime
without hardcoding dependencies between providers and consumers.
This mechanism is based on the existing ServiceLoader API, but modules make it reliable, explicit, and safe.
39.1 The Problem Services Solve
Sometimes a module needs to use a capability, but should not depend on a specific implementation.
Typical examples include: - logging frameworks - database drivers - plugin systems - service providers selected at runtime
Without services, the consumer would need to depend directly on a concrete implementation.
This creates tight coupling and reduces flexibility.
39.1.1 Roles in the Service Model
The JPMS service model involves four distinct roles.
| Role | Description |
|---|---|
Service interface |
Defines the contract |
Service provider |
Implements the service |
Service consumer |
Uses the service |
Service loader |
Discovers implementations at runtime |
39.1.2 Service Interface Module
The service interface defines the API that consumers depend on.
It must be exported so other modules can see it.
package com.example.service;
public interface GreetingService {
String greet(String name);
}
module com.example.service {
exports com.example.service;
}
Note
The service interface module should contain no implementations.
39.1.3 Service Provider Module
A provider module implements the service interface and declares that it provides the service.
package com.example.service.impl;
import com.example.service.GreetingService;
public class EnglishGreeting implements GreetingService {
public String greet(String name) {
return "Hello " + name;
}
}
module com.example.provider.english {
requires com.example.service;
provides com.example.service.GreetingService with com.example.service.impl.EnglishGreeting;
}
Key points:
- The provider depends on the service interface
- The implementation class does not need to be exported
- The provides directive registers the implementation
39.1.4 Service Consumer Module
The consumer module declares that it uses a service, but does not name any implementation.
module com.example.consumer {
requires com.example.service;
uses com.example.service.GreetingService;
}
Note
uses declares intent to discover implementations at runtime.
A module that declares uses but has no matching provider on the module path compiles normally,
but ServiceLoader returns an empty result at runtime.
39.1.5 Loading Services at Runtime
The ServiceLoader API performs service discovery.
It finds all providers visible to the module graph.
ServiceLoader<GreetingService> loader =
ServiceLoader.load(GreetingService.class);
for (GreetingService service : loader) {
System.out.println(service.greet("World"));
}
JPMS guarantees that only declared providers are discovered.
Classpath-based “accidental” discovery is prevented.
39.1.6 Service Resolution Rules
For a service to be discoverable by ServiceLoader, several conditions must be satisfied:
| Rule | Meaning |
|---|---|
| Provider module must be readable | Resolved by requires graph |
| Service interface must be exported | Consumers must see it |
Consumer must declare uses |
Otherwise ServiceLoader fails |
Provider must declare provides |
Implicit discovery is forbidden |
39.1.7 Service Locator Layer
It is possible to introduce an additional layer called Service Locator.
In this architecture:
- The
consumerdoes not directly useServiceLoader - The
Service Locatoris the only component that declaresuses - The
consumerdepends on theService Locator
Architectural structure:
Consumer → Service Locator → ServiceLoader → Provider
Service Locator module:
module com.example.locator {
requires com.example.service;
uses com.example.service.GreetingService;
}
Service Locator class:
package com.example.locator;
import java.util.ServiceLoader;
import com.example.service.GreetingService;
public class GreetingLocator {
public static GreetingService getService() {
return ServiceLoader
.load(GreetingService.class)
.findFirst()
.orElseThrow();
}
}
Consumer module:
module com.example.consumer {
requires com.example.locator;
}
The consumer does not declare uses because it does not directly invoke ServiceLoader.
39.1.8 Sequential Invocation Diagram
Execution sequence:
- The
ConsumerinvokesGreetingLocator.getService() - The
Service LocatorinvokesServiceLoader.load(...) - The
ServiceLoaderconsults the module graph - The system identifies modules that declare
provides - The
Providerimplementation is instantiated - The instance is returned to the
Consumer
Sequential diagram:
Consumer
│
│ 1. getService()
▼
Service Locator
│
│ 2. ServiceLoader.load()
▼
ServiceLoader
│
│ 3. Provider resolution
▼
Provider Implementation
│
│ 4. Instance returned
▼
Consumer
39.1.9 Component Summary Table
| Component | Role | exports | requires | uses | provides |
|---|---|---|---|---|---|
| SPI | Defines contract | ✅ | ❌ | ❌ | ❌ |
| Provider | Implements service | ❌ | ✅ | ❌ | ✅ |
| Service Locator | Performs discovery | (optional) | ✅ | ✅ | ❌ |
| Consumer | Uses the service | ❌ | ✅ | ❌ | ❌ |