Java Service Provider Interface (SPI)
SPI (Service Provider Interface) is a service discovery mechanism that provides extension points for interface implementations. Introduced in JDK 6 (see the Since field of java.util.ServiceLoader), SPI enables dynamic loading of concrete service providers at runtime, promoting decoupling and offernig Inversion of Control (IoC) like behavior.
The typical implementation pattern requires provider JARs to contain a META-INF/services directory. Inside that directory, a text file named after the full qualified name of the SPI interface lists the fully qualified class names of its implementations (one per line, UTF-8 encoded).
Benefits and Limitations
- Separation of concerns: The interface definition is separated from its implementations.
- Extensibility: New implementations can be added simply by including a new JAR on the classpath.
- Limitation: SPI loads all implementations at once, regardless of whether they are needed. This can cause performance overhead and memory waste.
API vs SPI
API (Application Programming Interface) is provided to consumers. SPI is provided by an interface for implementors. In many frameworks, the API internally uses SPI to locate the appropriate implementation from the current classpath.
Examples in Practice
1. JDBC Driver Loading
The java.sql.Driver interface is a classic SPI. JDK only defines the interface; database vendors supply their own jar files containing implementations. At runtime, the JDBC DriverManager discovers these implementations via the SPI mechanism.
For example, mysql-connector-java-8.x.jar contains a file META-INF/services/java.sql.Driver whose content are com.mysql.cj.jdbc.Driver. The MySQL Driver implementation registers itself with DriverManager inside a static initializer:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException e) {
throw new RuntimeException("Can't register driver!", e);
}
}
public Driver() throws SQLException { }
}
The PostgreSQL driver does the same: org.postgresql.Driver also calls DriverManager.registerDriver() in its static block.
Inside the JDK’s DriverManager, the method ensureDriversInitialized() first loads drivers from the system property jdbc.drivers, then uses ServiceLoader to enumerate all implementations of java.sql.Driver found in META-INF/services:
private static void ensureDriversInitialized() {
if (driversInitialized) return;
synchronized (lockForInitDrivers) {
// Load from system property
String drivers = AccessController.doPrivileged(
(PrivilegedAction<String>) () -> System.getProperty("jdbc.drivers")
);
// Use ServiceLoader to discover SPI implementations
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> iter = loadedDrivers.iterator();
try {
while (iter.hasNext()) {
iter.next(); // triggers instantiation
}
} catch (Throwable t) { /* ignore */ }
return null;
});
// Also handle jdbc.drivers system property
if (drivers != null && !drivers.isEmpty()) {
for (String d : drivers.split(":")) {
try {
Class.forName(d, true, ClassLoader.getSystemClassLoader());
} catch (Exception ex) { /* log only */ }
}
}
driversInitialized = true;
}
}
2. Spring's spring.factories
Spring has a similar mechanism using META-INF/spring.factories files, processed by SpringFactoriesLoader (available since Spring 3.2). This is the foundation of Spring Boot auto‑configuration.
For example, in spring-boot-actuator-autoconfigure-3.2.4.jar:
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.actuate.autoconfigure.metrics.ValidationFailureAnalyzer,\
org.springframework.boot.actuate.autoconfigure.health.NoSuchHealthContributorFailureAnalyzer
The file uses Java .properties format. Keys are interfaces/abstract classes; values are comma‑separated implementation class names. Lines ending with backslash continue on the next line.
SpringFactoriesLoader caches its results per classloader. Its main methods are:
forResourceLocation(String resourceLocation, ClassLoader classLoader)– retrieves or creates a loader for a specific resource.load(Class<T> factoryType, ...)– returns a list of instances for the given factory type, ordered by@OrderorOrdered.
Key internal fields:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
static final Map<ClassLoader, Map<String, SpringFactoriesLoader>> cache = new ConcurrentReferenceHashMap<>();
private final Map<String, List<String>> factories; // key = interface FQN, value = implementation class list
3. Dubbo's Extension Mechanism
Dubbo extends the SPI idea with its own ExtensionLoader and @SPI annotation. The framework looks for configuration files in three locations (in this order):
META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/
The LoadingStrategy implementations (e.g., DubboInternalLoadingStrategy) control these paths. The file format is key=fully.qualified.ClassName (one per line).
The @SPI annotation:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
String value() default "";
ExtensionScope scope() default ExtensionScope.APPLICATION;
}
The core class ExtensionLoader<T> provides the getExtension(String name) method which obtains a named extension instance (or the default one if "true" is passed). It uses a caching mechanism with Double‑Checked Locking.
public T getExtension(String name, boolean wrap) {
checkDestroyed();
if (StringUtils.isEmpty(name)) throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) return getDefaultExtension();
String cacheKey = wrap ? name : name + "_origin";
final Holder<Object> holder = getOrCreateHolder(cacheKey);
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name, wrap);
holder.set(instance);
}
}
}
return (T) instance;
}
The createExtension method obtains the correct class via getExtensionClasses().get(name), creates an instance, injects dependencies (injectExtension), handles wrapping (for decorators), and finally calls initExtension.
The template method loadExtensionClasses iterates over all LoadingStrategy instances and loads the directory for the given interface:
private Map<String, Class<?>> loadExtensionClasses() throws InterruptedException {
checkDestroyed();
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy, type.getName());
// backward compatibility for old ExtensionFactory
if (this.type == ExtensionInjector.class) {
loadDirectory(extensionClasses, strategy, ExtensionFactory.class.getName());
}
}
return extensionClasses;
}
The actual parsing of configuration files happens in loadResource and loadClass, which populate caches such as cachedAdaptiveClass, cachedWrapperClasses, and cachedNames.
Reference
Original blog post by the user (rewritten for clarity and structure).