The Service Provider Interface (SPI) in Java allows components to be created and loaded dynamically at runtime.
It provides a mechanism for implementing and discovering service implementations without directly coupling them to the calling code.
This mechanism is especially important in modular design, and its core idea is decoupling.
What You Need
- About 5 minutes
- A favorite text editor or IDE
- Java 8 or later
1. Main Components
The SPI consists of three main components :
- Service Interface : this is the interface that defines the contract or API that service implementations must adhere to. It represents the common functionality that is expected from the service;
- Service Provider Interface : this is an interface or abstract class that acts as a factory or provider for creating instances of the service implementation. It typically contains methods to create or retrieve instances of the service;
- Service Implementation : these are the concrete implementations of the service interface. Each implementation provides its own specific implementation of the service contract.
The SPI is supported by the java.util.ServiceLoader class, which is responsible for loading and instantiating service providers based on a specified service interface.
To use SPI, typically it needs to define a service interface, implement one or more service providers, and provide a configuration file (META-INF/services/) that lists the fully qualified class names of the service providers.
The ServiceLoader then loads and provides access to the service providers.
The SPI is widely used in Java frameworks and libraries to provide pluggable components and extensions, allowing developers to easily customize and extend the behavior of their applications.
2. Simple Example
Let’s assume we have a service interface called Search that defines a common search of keyword functionality :
public interface Search {
void search(String keyword);
}
Now, we can have multiple implementations of this Search interface, each providing its own searching mechanism.
For example, let’s create first implementation FileSearch which searches keyword in a file :
public class FileSearch implements Search {
@Override
public void search(String keyword) {
System.out.println("Searching '" + keyword + "' In File");
}
}
Then, second implementation DatabaseSearch which searches keyword in a database :
public class DatabaseSearch implements Search {
@Override
public void search(String keyword) {
System.out.println("Searching '" + keyword + "' In Database");
}
}
To make these implementations discoverable through the SPI, we need to create a configuration file named META-INF/services/Search.
In this file, we list the fully qualified class names of the implementations :
- FileSearch
- DatabaseSearch
Now, we can use the ServiceLoader class to load and access the available service providers.
It automatically loads and instantiates the service providers listed in the configuration file : META-INF/services.
import java.util.Iterator;
import java.util.ServiceLoader;
public class SPI {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.search("hello world");
}
}
}
The output of above code snippet is like below :
Searching 'hello world' In File
Searching 'hello world' In Database
3. Widely Used Examples
3.1 JDBC DriverManager
Before JDBC4.0, when we want to have a connection database, we usually use Class.forName() like below to load the database-related driver first and then perform operations such as obtaining the connection.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class JDBCTest {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
String url = "jdbc:mysql://localhost:3306/students";
String username = "gigi";
String password = "1234";
String query = "select *from students";
Class.forName("com.mysql.cj.jdbc.Driver");
Connection con = DriverManager.getConnection(url, username, password);
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(query);
rs.next();
String name = rs.getString("name");
System.out.println(name);
st.close();
con.close();
}
}
After JDBC4.0, there is no need to use Class.forName() to load the driver, we can just get the connection directly.
Because now this method is realized by using the Java SPI extension mechanism.
The JDBC interface is defined in java java.sql.Driver, and there is no specific implementation.
The implementations of this interface are provided by different manufacturers.
For instance, in the jar package of mysql-connector-java-6.0.6.jar, there is a file META-INF/services.
The content of the file is the implementation of the interface java.sql.Driver : com.mysql.cj.jdbc.Driver.
The same configuration file can also be found in the jar package of postgresql-42.0.0.jar.
The content of the file is org.postgresql.Driver, which is the implementation of postgresql for Java.
3.2 Apache Commons Logging
In Apache Commons Logging, the log instance is created by the getLog(String) method of LogFactory.
The return type of the getLog(String) method is the org.apache.commons.logging.Log interface.
LogFatory is an abstract class that is responsible for loading specific log implementations.
The concrete log implementation can be defined in org.apache.commons.logging.LogFactory file in the META-INF/services directory.
In this way, LogFactory uses the SPI service discovery mechanism to discover the log implementation.
3.3 SpringBoot
In the automatic assembly process of springboot, SpringFactoriesLoader is another example of SPI service discovery mechanism.
It is in charge of searching and loading all the configurations defined in the file META-INF/spring.factories in the classpath.
4. Limitations of SPI
The SPI mechanism has below limitations :
- It cannot be loaded on demand, and all implementations need to be instantiated, for some implementation that we do not want to use, it is also loaded and instantiated, which causes waste;
- The ServiceLoader class is not thread-safe, which may causes trouble in multi-thread context;
- The way to obtain a certain implementation is not flexible enough, it can only be obtained in the form of Iterator, and the corresponding implementation cannot be obtained according to a certain parameter.