Contents

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.