Contents
- Defining a Service Interface
- Registering Providers (META-INF/services)
- Loading and Using Services
- Lazy Loading with Stream (Java 9+)
- Module System Integration
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"; }
}
Providers are registered in a file under META-INF/services/ inside the provider's JAR. The filename is the fully-qualified service interface name, and each line is a provider implementation class name.
// 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
ServiceLoader.load() scans the classpath (or module path) for all registered implementations of the given service interface and returns a lazy ServiceLoader iterable. Implementations are instantiated lazily — only when the iterator reaches them — so loading a service does not eagerly construct all providers. This makes ServiceLoader well-suited for plugin systems where implementations are provided by third-party JARs dropped onto the classpath, and the core application has no compile-time dependency on them.
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();
Java 9 added ServiceLoader.Provider<T> and a stream() method for lazy, stream-based access. Providers are instantiated only when needed:
// 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());
In the Java Module System (Java 9+), META-INF/services still works, but the preferred approach is to use module-info.java declarations which are more explicit and module-aware:
// 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);
ServiceLoader powers many JDK subsystems: java.sql.Driver (JDBC), javax.imageio.spi.ImageReaderSpi, java.nio.charset.spi.CharsetProvider, SLF4J backends, and more. It's the idiomatic Java way to build extension points without reflection or a DI container.