我正在参加「掘金·启航计划」
定义
SPI(Service Provider Interface) 是一种面向接口编程的技术,它可以让一个程序根据接口约定规范自动发现和加载对应的实现类。它是一种 Java 种的接口编程规范,它定义了接口和服务提供者之间的约定规范,使得在运行时动态加载实现该接口的类。SPI 机制是通过在服务提供者接口上定义注解和在配置文件种指定实现类的方式来实现的。
如何实现
当服务的提供者提供了一种接口的实现之后,需要在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体实现类。当其他程序需要这个服务的时候,就会找到这个类并且实例化。JDK 中查找服务的实现的工具类是:java.util.ServiceLoader。
简单demo实现
定义一个接口,提供两个实现类:
public interface Plugin {
void execute();
}
public class PluginA implements Plugin {
@Override
public void execute() {
System.out.println("PluginA.execute is done");
}
}
public class PluginB implements Plugin {
@Override
public void execute() {
System.out.println("PluginB.execute is done");
}
}
配置好文件:
测试:
public class PluginTest {
public static void main(String[] args) {
ServiceLoader<Plugin> serviceLoader=ServiceLoader.load(Plugin.class);
Iterator<Plugin> itre = serviceLoader.iterator();
while(itre.hasNext()){
//itre.next() 这行代码会对 SPI 配置的实现类进行初始化
itre.next().execute();
}
}
}
//打印结果:
//PluginA.execute is done
//PluginB.execute is done
扩展
很多功能都使用了 SPI 机制:比如 JDBC DriverManger 、Spring 、MyBatis、log 日志、Dubbo
下面简单介绍几种。
JDBC DriverManager
最开始的时候,我们连接数据库的时候,需要先加载驱动:Class.forName("com.mysql.cj.jdbc.Driver"),然后再是获取连接的操作。JDBC4.0 之后引入了 SPI 就不需要手动加载驱动了,直接获取连接即可。
String url = "jdbc:mysql://localhost:3306/xxx?useUnicode=true";
Connection connection = DriverManager.getConnection(url, "root", "root");
System.out.println(connection);
按照上面定义的规范,可以看看 mysql jar 包中是否有对应的配置:
在 META-INF/services/ 目录下有一个服务接口命名的文件(java.sql.Driver)以及里面的内容是 MySQL jar 包里面的具体实现类。
DriverManager 中的静态代码块:里面会有 ServiceLoader.load(Driver.class); 方法去加载配置类的,在循环的时候就会实例化配置的类:driversIterator.next(); 这里才会真正实例化。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
下面是 ServiceLoader#nextService() 方法,里面会加载类 Class.forName() 并且下面会调用 newInstance() 去初始化。
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
Spring 中 SPI 机制
在 SpringBoot 自动装配的过程中,最终会加载 META-INF/spring.factories 文件,加载过程是由 SpringFactoriesLoader 实现。
整个代码就是去找到 spring.factories 文件,解析里面的内容,最终将配置的实现类实例化,并返回一个 List。
MyBatis 中的插件
MyBatis 中的插件是使用 SPI 机制思想,不是使用的 JDK SPI 。MyBatis 专门独立一个模块 plugin 来实现插件的扩展。MyBatis 里面只有四个接口可扩展:Executor、ParameterHandler、ResultSetHandler、StatementHandler 。如果要实现扩展一个 MyBatis 接口需要做以下工作:
- 实现 Interceptor 接口,并且需要通过 @Intercepts 和 @Signature 接口指定拦截什么接口的什么方法
- 在配置文件中配置 标签,声明: 这样MyBatis 初始化的时候就会加载到 Configuration (MyBatis 配置对象)中。
当 MyBatis 执行 SQL 时,会按照插件的配置顺序依次调用插件的 intercept() 方法来对 SQL 语句进行处理,从而实现对 SQL 的拦截和增强。
SPI 使用
- 组织或者公司定义标准
- 厂商各自实现
- 开发者调用
比如就是 Java 定义 Driver 接口,MySQL 厂商实现 mysql.Driver 实现类,我们通过 DriverManager 获取 mysql 的连接。
总结
优点:SPI 核心思想就是解耦。我只定义标准,具体实现由不同的厂商实现。
缺点:
- 不能按需加载,必须遍历所有实现并初始化,但是有点初始化可能会很耗时
- 获取某个实现类的方式不够灵活,只能遍历获取
- 多线程使用 ServiceLoader 不安全