Understanding Java SPI Mechanism
Project Address: Code Repository
[Projects in the project starting with spi]
1. Java SPI Mechanism
SPI (Service Provider Interface) is a standard in Java that provides a service discovery mechanism allowing for the dynamic discovery and loading of service implementation classes at runtime without explicit binding at compile time. It is widely used in the Java ecosystem (such as JDBC driver loading, logging frameworks, etc.).
① Core Idea of SPI
- Decoupling Interface and Implementation: Define a common interface (
<span>Service Provider Interface</span>), which can have different versions implemented by different vendors or modules. - Dynamic Loading: At runtime, find all implementation classes through configuration files (
<span>META-INF/services/fully.qualified.interface.name</span>) and load them. - Strong Extensibility: New service implementations can be added without modifying the main program code.
② Classic Example: JDBC Driver Loading
The loading of JDBC drivers is a typical application of SPI:
- Interface:
<span>java.sql.Driver</span> - Implementation Classes: Drivers from various database vendors (such as MySQL’s
<span>com.mysql.cj.jdbc.Driver</span>) - Configuration File:
<span>META-INF/services/java.sql.Driver</span>lists all driver class names.
③ Custom Example
Without modifying code, call different implementations based on the loaded packages.
First, define the interface provider
Create a project<span>spi-provider</span>
which only defines one interface UserService
// Interface exposed by SPI
public interface UserService {
String hello(String name);
}
Then create two different implementations
- Create a project
<span>spi-impl-mysql</span>, introducing<span>spi-provider</span>
public class MysqlUserImpl implements UserService {
@Override
public String hello(String name) {
return "【MySQL】:" + name;
}
}
In the resources directory of this project, create the following directory
resources
--META-INF
----services
------com.feng.spi.UserService [this is the file]
The content of the file is
com.feng.impl.MysqlUserImpl
- Create a project
<span>spi-impl-redis</span>, introducing<span>spi-provider</span>
public class RedisUserImpl implements UserService {
@Override
public String hello(String name) {
return "【Redis】 " + name;
}
}
In the resources directory of this project, create the following directory
resources
--META-INF
----services
------com.feng.spi.UserService [this is the file]
The content of the file is
com.feng.impl.RedisUserImpl
Create a new project for testing
Create<span>spi-use</span> project
// Load with this class
public class UserServer {
private static final List<UserService> services = new ArrayList<>();
private static final UserServer userServer = new UserServer();
private UserServer(){
ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class); // Load implementation classes
for (UserService userService : userServices) {
services.add(userService);
}
}
public static String hello(String name){
if (services.isEmpty()) {
// System.err.println("No UserService implementation found");
return "No UserService implementation found";
}
return services.get(0).hello(name);
}
}
// Run this class to test
public class App {
public static void main(String[] args) {
String hello = UserServer.hello("田小锋");
System.err.println(hello);
}
}
First step,<span>spi-use</span> first introduces the mysql project
<dependency>
<groupId>com.feng.impl</groupId>
<artifactId>spi-impl-mysql</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
Run the test:
The output is: 【MySQL】:田小锋
Second step, comment out the mysql dependency, and introduce redis’s
<dependency>
<groupId>com.feng.impl</groupId>
<artifactId>spi-impl-redis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
Run the test:
The output is: 【Redis】 田小锋
After changing the dependencies in the pom file, remember to reload the pom file.
This way, by changing dependencies, we did not change the java code, but achievedimporting different scenarios can achieve different functionalities. Doesn’t it feel a bit like Spring Boot?
From the above custom example, we can summarize the implementation method of Java’s SPI mechanism:
- Define the interface;
- Provide implementation;
- Configuration file: META-INF/services/fully.qualified.interface.name (file), with the content being the fully qualified name of the specific implementation class;
- Use, load services;
④ Java SPI Principle
It is very easy to see that the following lines of code are the core.
ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class); // Load implementation classes
for (UserService userService : userServices) {
services.add(userService);
}
Let’s analyze its source code step by step from the beginning.
public final class ServiceLoader<S> implements Iterable<S> {
....
}
<span>It calls the load(Class<S> service) method</span>
public static <S> ServiceLoader<S> load(Class<S> service) {
// Get the context class loader of the current thread
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// Call the overloaded load method
return ServiceLoader.load(service, cl);
}
<span>It calls the overloaded load(Class<S> service, ClassLoader loader) method</span>
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader); // Create a new object
}
<span>Constructor ServiceLoader(Class<S> svc, ClassLoader cl)</span>
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// Check for null
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// If the passed context class loader is null, use the system class loader
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
// This is not very clear, but not important for this article
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// This is the key!!!!!!!!!
reload();
}
<span>reload() method</span>
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader); // Create an object, then it disappears=====
}
However, ServiceLoader implements the Iterable interface, so when iterating with for, it will implicitly call iterator() to get the iterator.
Let’s look at the iterator() method implemented by ServiceLoader
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext(); // ================
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next(); // ================
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
We can see that in the reload() method,<span>lookupIterator = new LazyIterator(service, loader;</span> object takes effect!
LazyIterator is an inner class of ServiceLoader, implementing the Iterator interface. Next, let’s look at hasNext() and next() of LazyIterator;
private class LazyIterator implements Iterator<S>{
public boolean hasNext() {
if (acc == null) {
return hasNextService();//========================= Key point
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService();} //========================= Key point
};
...........
}
}
public S next() {
if (acc == null) {
return nextService();//========================= Key point
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService();} //========================= Key point
};
.......
}
}
// Key point------------------------
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX = "META-INF/services/"
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
...........
return true;
}
// Key point----------------------------
private S nextService() {
...
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// Load
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
....
}
.......
try {
// c.newInstance()
S p = service.cast(c.newInstance());
providers.put(cn, p); // Put into LinkedHashMap
return p;
} .......
}
}
Three important points:
-
Through source code analysis. PREFIX = “META-INF/services/”, is a convention. Therefore, it must be written this way
-
The underlying mechanism is still reflection. c.newInstance() creates an object using the no-argument constructor.
-
Iterator design pattern
So, does SPI have any drawbacks? Each time it loads, it has to read the file, which may cause performance issues, but the actual impact may not be significant. Also, it can only instantiate classes through a no-argument constructor, which may be inconvenient if the implementation class requires parameters. Additionally, if there are multiple implementations, you need to choose which one to use yourself, as ServiceLoader simply iterates through all implementations, and you may need to use certain conditions to determine which one to choose.
2. Spring Boot SPI Mechanism
In the part of the custom example above, didn’t we say this?
We did not change the Java code, but achievedimporting different scenarios can achieve different functionalities. Doesn’t it feel a bit like Spring Boot?
That’s right! Spring Boot deeply integrates and extends Java’s SPI mechanism, forming its own auto-configuration system. Its core idea is similar to Java SPI, but it simplifies the service discovery and dependency injection process further through annotations and conditional programming.
It provides a way to decouple container injection, helping external packages (independent of Spring Boot projects) register Beans into the Spring Boot project container.
In the Spring Boot principle analysis article, we have already learned about this.
In version 2.x, it scans the META-INF/spring.factories file [In versions 2.7 and later, EnableAutoConfiguration has been moved to META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports]. This is just a brief overview.
# spring.factories
# Key: Fully qualified class name of the interface to be extended
# Value: Fully qualified class name of the implementation class (multiple implementation classes separated by commas)
# For example
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
Spring Boot uses many factories mechanisms by default, mainly including: [copied from others]
- ApplicationContextInitializer: A callback interface for initializing Spring ConfigurableApplicationContext before the Spring container refreshes.
- ApplicationListener: Used to handle events during various stages of container initialization.
- AutoConfigurationImportListener: When importing configuration classes, it retrieves detailed information about the classes (Listener that can be registered with spring.factories to receive details of imported auto-configurations).
- AutoConfigurationImportFilter: Used to conditionally filter imported configuration classes (Filter that can be registered in spring.factories to limit the auto-configuration classes considered. This interface is designed to allow fast removal of auto-configuration classes before their bytecode is even read).
- EnableAutoConfiguration: Specifies the list of configuration classes to be auto-loaded (Enable auto-configuration of the Spring Application Context, attempting to guess and configure beans that you are likely to need. Auto-configuration classes are usually applied based on your classpath and what beans you have defined. For example, if you have tomcat-embedded.jar on your classpath you are likely to want a TomcatServletWebServerFactory (unless you have defined your own ServletWebServerFactory bean).
- FailureAnalyzer: Intercepts exceptions during startup and converts them into readable messages, including them in FailureAnalysis. Spring Boot provides this kind of analyzer for application context-related exceptions, JSR-303 validation, etc. (A FailureAnalyzer is used to analyze a failure and provide diagnostic information that can be displayed to the user).
- TemplateAvailabilityProvider: Template engine configuration. (Collection of TemplateAvailabilityProvider beans that can be used to check which (if any) templating engine supports a given view. Caches responses unless the spring.template.provider.cache property is set to false).
① Core Mechanism of Spring Boot SPI
1. Service discovery based on <span>spring.factories</span>
- Configuration File Location:
<span>META-INF/spring.factories</span> - Function: Used to declare Spring Boot’s auto-configuration classes (
<span>@Configuration</span>classes) or SPI implementation classes. - Format: Each line is defined in the form of
<span>key=value</span>, where the key is usually the fully qualified name of the interface, and the value is the fully qualified name of the implementation class.
Spring Boot scans the classes in <span>spring.factories</span> and loads them as auto-configuration classes into the Spring container.
2. Conditional Annotation Driven
Spring Boot uses annotations like <span>@ConditionalOnMissingBean</span>, <span>@ConditionalOnClass</span> to dynamically control whether auto-configuration takes effect.
② Workflow of Spring Boot SPI
- Scanning at Startup: Spring Boot scans all classes under all class loaders through the
<span>SpringFactoriesLoader.loadFactoryClasses()</span>method for the<span>spring.factories</span>file. - Loading Auto-Configuration Classes: Instantiates the classes declared in
<span>spring.factories</span>as<span>Configuration</span>objects and registers them in the Spring container. - Conditional Filtering: Uses
<span>@Conditional</span>annotations (such as<span>@ConditionalOnClass</span>,<span>@ConditionalOnProperty</span>) to filter the auto-configuration classes that meet the conditions. - Dependency Injection: Injects the objects generated by the
<span>@Bean</span>methods in the auto-configuration classes into the Spring container.
③ Spring Boot SPI vs Java SPI
| Feature | Spring Boot SPI | Standard Java SPI |
|---|---|---|
| Configuration Method | Through <span>spring.factories</span> file + <span>@Configuration</span> |
Through <span>META-INF/services/</span> configuration file |
| Dependency Injection | Automatically registers implementation classes as Spring Beans | Must manually call <span>ServiceLoader</span> and manage instances |
| Conditional Support | Supports rich conditional annotations (such as <span>@ConditionalOnMissingBean</span>) |
No conditional filtering mechanism |
| Integration with Ecosystem | Deeply integrated into Spring Boot’s auto-configuration system | Independent of Spring framework |
④ Custom Example
See the project inside the 【spi-springboot project】
Still using the above example. We create projects to implement the interface separately.
First, our custom project scenario is not included in Spring’s official collection and is not in its auto-configuration scenario package, so we can only mimic the @SpringBootApplication annotation and import beans via @Import.
<span>spi-mysql-starter project</span>
@Service("mysqlUserService")
public class MysqlUserServiceImpl implements UserService {
@Override
public String hello(String name) {
return "【SpringBoot MySQL】:" + name;
}
} // Define implementation class
Then create the META-INF/spring.factories file under the resources directory
# Key: Fully qualified class name of the interface to be extended
# Value: Fully qualified class name of the implementation class (multiple implementation classes separated by commas)
com.feng.spi.UserService=com.feng.spimysql.MysqlUserServiceImpl
<span>spi-redis-starter project</span>
@Service("redisUserService")
public class RedisUserServiceImpl implements UserService {
@Override
public String hello(String name) {
return "【SpringBoot Redis】:" + name;
}
}
Similarly
com.feng.spi.UserService=com.feng.spiredis.RedisUserServiceImpl
<span>In the spi-springboot-use project</span>
Introduce either or both of the above mysql and redis.
Look at the effect in the startup class
@SpringBootApplication
public class App {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(App.class, args);
// Introduce one of them
try{
UserService bean = context.getBean(UserService.class);
System.err.println(bean.hello("田小锋"));
System.err.println("====================");
} catch (Exception e ) {
System.err.println("More than one UserService");
}
}
}

As shown in the figure above, first introduce mysql.

Next, change a dependency and introduce redis. See the figure below

In this way, a simple example is completed, importing different scenarios can allow the beans in the container to have different functionalities.
3. References
[Java’s SPI mechanism]: https://blog.csdn.net/sigangjun/article/details/79071850
[Introduction to Spring’s Factories mechanism]: https://segmentfault.com/a/1190000042247124
deepseek