The Service Provider Interface (SPI) is a powerful service discovery mechanism built into Java. It enables frameworks to discover and load service implemantations dynamically by scanning configuration files in the META-INF/services directory on the classpath.
Many popular frameworks including Dubbo and JDBC leverage SPI to provide extensible architectures.
Practical Example
First, define a service interface:
public interface MessageProcessor {
void process();
}
Create the first implementation:
public class TextMessageProcessor implements MessageProcessor {
@Override
public void process() {
System.out.println("TextMessageProcessor");
}
}
Create a second implementation:
public class XmlMessageProcessor implements MessageProcessor {
@Override
public void process() {
System.out.println("XmlMessageProcessor");
}
}
You can load these implementations using either ServiceLoader.load() from java.util or Service.providers() from sun.misc:
public class SpiDemo {
public static void main(String[] args) {
// Using ServiceLoader
ServiceLoader<MessageProcessor> loader = ServiceLoader.load(MessageProcessor.class);
// Using Service.providers
Iterator<MessageProcessor> providers = Service.providers(MessageProcessor.class);
System.out.println("=== ServiceLoader Results ===");
for (MessageProcessor processor : loader) {
processor.process();
}
System.out.println("=== Service.providers Results ===");
while (providers.hasNext()) {
MessageProcessor processor = providers.next();
processor.process();
}
}
}
Configuration File Setup
Create a file in META-INF/services/ named after the fully qualified interface class name:
File: META-INF/services/com.example.MessageProcessor
com.example.TextMessageProcessor
com.example.XmlMessageProcessor
Implementation Discovery Flow
The ServiceLoader class manages the loading process with these key components:
public final class ServiceLoader<S> implements Iterable<S> {
private static final String PREFIX = "META-INF/services/";
private final Class<S> service;
private final LinkedHashMap<String, S> implementations = new LinkedHashMap<>();
private final ClassLoader classLoader;
private LazyIterator lookupIterator;
}
When load() is called, it initializes the internal iterator and prepares for lazy loading of implementations:
private ServiceLoader(Class<S> svc, ClassLoader cl) {
this.service = Objects.requireNonNull(svc, "Service interface cannot be null");
this.classLoader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
this.implementations.clear();
this.lookupIterator = new LazyIterator(service, classLoader);
}
The iterator returned by iterator() delegates to the internal LazyIterator:
public Iterator<S> iterator() {
return new Iterator<S>() {
public boolean hasNext() {
return lookupIterator.hasNext();
}
public S next() {
return lookupIterator.next();
}
};
}
The LazyIterator performs the actual discovery by reading configuration files:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
String configFileName = PREFIX + service.getName();
configs = classLoader.getResources(configFileName);
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
When next() is invoked, the iterator instantiates the discovered class using reflection:
private S nextService() {
String className = nextName;
nextName = null;
Class<?> implClass = Class.forName(className, false, classLoader);
S instance = service.cast(implClass.getDeclaredConstructor().newInstance());
implementations.put(className, instance);
return instance;
}
Summary
The SPI mechanism provides a standardized way to discover and load service implementations at runtime. By following the convention of placing configuraton files in META-INF/services/, developers can create extensible frameworks without hardcoding dependencies.