Java ServiceLoader: Built-in Plugin and SPI Mechanism
ServiceLoader is Java's standard Service Provider Interface (SPI) mechanism. It lets you define an interface, register implementations in separate JARs without touching the core application, and discover them at runtime: all without a framework like Spring or Guice. The JDK itself uses it extensively: JDBC drivers, JCE providers, logging backends, and many more.
Contents
Defining a Service Interface
A service is just a regular Java interface (or abstract class). Providers implement it. The interface is typically in a shared API module.
// Service interface — defines the contract
public interface MessageFormatter {
String format(String message);
String name(); // human-readable provider name
}
// Provider 1 — JSON formatter
public class JsonMessageFormatter implements MessageFormatter {
@Override
public String format(String message) {
return "{\"message\":\"" + message + "\"}";
}
@Override public String name() { return "JSON"; }
}
// Provider 2 — XML formatter
public class XmlMessageFormatter implements MessageFormatter {
@Override
public String format(String message) {
return "<message>" + message + "</message>";
}
@Override public String name() { return "XML"; }
}
// Provider 3 — Plain text (identity)
public class PlainMessageFormatter implements MessageFormatter {
@Override
public String format(String message) { return message; }
@Override public String name() { return "Plain"; }
}
Registering Providers (META-INF/services)
Providers are registered in a file under META-INF/services/
// File: src/main/resources/META-INF/services/com.example.MessageFormatter
// Contents (one fully-qualified class name per line):
com.example.JsonMessageFormatter
com.example.XmlMessageFormatter
com.example.PlainMessageFormatter
// Directory structure:
// my-plugin.jar
// └── META-INF/
// └── services/
// └── com.example.MessageFormatter ← the registration file
// Each provider JAR can register its own implementations independently.
// The JDK uses this for JDBC: adding a JDBC driver JAR to the classpath
// automatically registers it — no code change needed.
// Provider requirements (classic ServiceLoader):
// 1. Must implement (or extend) the service interface/class
// 2. Must have a public no-arg constructor
// 3. Fully-qualified class name must be on the classpath at runtime
Loading and Using Services
ServiceLoader.load()ServiceLoaderServiceLoader
import java.util.ServiceLoader;
// Load all providers for MessageFormatter
ServiceLoader<MessageFormatter> loader =
ServiceLoader.load(MessageFormatter.class);
// Iterate all providers (lazy — each loaded on first access)
for (MessageFormatter formatter : loader) {
System.out.println(formatter.name() + ": " + formatter.format("hello"));
}
// JSON: {"message":"hello"}
// XML: <message>hello</message>
// Plain: hello
// Find first available provider
MessageFormatter first = ServiceLoader.load(MessageFormatter.class)
.findFirst()
.orElseThrow(() -> new RuntimeException("No formatter found"));
// Find by name
MessageFormatter json = StreamSupport
.stream(ServiceLoader.load(MessageFormatter.class).spliterator(), false)
.filter(f -> "JSON".equals(f.name()))
.findFirst()
.orElseThrow();
// Load with a specific ClassLoader (useful in OSGi or custom class loaders)
ServiceLoader<MessageFormatter> custom =
ServiceLoader.load(MessageFormatter.class, MyPlugin.class.getClassLoader());
// Reload — forces re-scanning (useful during testing or hot-reload)
loader.reload();
Lazy Loading with Stream (Java 9+)
Java 9 added ServiceLoader.Provider<T>stream()
// stream() returns Stream<ServiceLoader.Provider<T>>
// Provider.get() instantiates the implementation (only when called)
ServiceLoader.load(MessageFormatter.class)
.stream()
.filter(p -> p.type().getSimpleName().startsWith("Json"))
.map(ServiceLoader.Provider::get) // instantiate only the matching one
.findFirst()
.ifPresent(f -> System.out.println(f.format("test")));
// Provider.type() gives the Class<T> without instantiating
// Useful for inspecting annotations on the implementation class
ServiceLoader.load(MessageFormatter.class)
.stream()
.map(ServiceLoader.Provider::type)
.map(Class::getSimpleName)
.forEach(System.out::println); // prints class names, no instances created
// Collecting all available providers
List<MessageFormatter> all = ServiceLoader.load(MessageFormatter.class)
.stream()
.map(ServiceLoader.Provider::get)
.collect(java.util.stream.Collectors.toList());
Module System Integration
In the Java Module System (Java 9+), META-INF/servicesmodule-info.java
// API module — declares the service
// module-info.java in module "com.example.api"
module com.example.api {
exports com.example.api; // exports the interface package
uses com.example.api.MessageFormatter; // declares it uses this service
}
// Provider module — implements the service
// module-info.java in module "com.example.json"
module com.example.json {
requires com.example.api;
// No need to export the implementation package
provides com.example.api.MessageFormatter
with com.example.json.JsonMessageFormatter,
com.example.json.AnotherJsonFormatter;
}
// Provider with a factory method (Java 9+)
// Provider class can have a static provider() factory instead of no-arg constructor
public class JsonMessageFormatter implements MessageFormatter {
private JsonMessageFormatter() {} // private constructor — no problem
public static JsonMessageFormatter provider() { // factory method
return new JsonMessageFormatter();
}
// ...
}
// Consumer code is unchanged — ServiceLoader finds providers automatically
ServiceLoader<MessageFormatter> loader =
ServiceLoader.load(MessageFormatter.class);
ServiceLoaderjava.sql.Driverjavax.imageio.spi.ImageReaderSpijava.nio.charset.spi.CharsetProvider