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.

Java
// 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/ inside the provider's JAR. The filename is the fully-qualified service interface name, and each line is a provider implementation class name.

Java
// 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() 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.

Java
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> and a stream() method for lazy, stream-based access. Providers are instantiated only when needed:

Java
// 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/services still works, but the preferred approach is to use module-info.java declarations which are more explicit and module-aware:

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);
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.