什么是SPI机制
- SPI是JDK内置的一种服务提供发现机制,主要是被框架的开发人员使用,比如java.sql.Driver接口,不同厂商可以针对同一接口做出不同的实现,例如:MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外。
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
当调用方需要调用这个接口时,就可以通过上述路径去查找对应接口的实现类,然后去加载这个实例化。
Demo
1.定义一个接口HelloSPI。
package com.vivo.study.spidemo.spi;
public interface HelloSPI {
void sayHello();
}
2.完成接口的多个实现。
package com.vivo.study.spidemo.spi.impl;
import com.vivo.study.spidemo.spi.HelloSPI;
public class ImageHello implements HelloSPI {
public void sayHello() {
System.out.println("Image Hello");
}
}
package com.vivo.study.spidemo.spi.impl;
import com.vivo.study.spidemo.spi.HelloSPI;
public class TextHello implements HelloSPI {
public void sayHello() {
System.out.println("Text Hello");
}
}
在META-INF/services/目录里创建一个以com.vivo.study.spidemo.spi.HelloSPI的文件,这个文件里的内容就是这个接口的具体的实现类。
具体内容如下:
com.vivo.study.spidemo.spi.impl.ImageHello
com.vivo.study.spidemo.spi.impl.TextHello
3.使用 ServiceLoader 来加载配置文件中指定的实现
package com.vivo.study.spidemo.test
import java.util.ServiceLoader;
import com.vivo.study.spidemo.spi.HelloSPI;
public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<HelloSPI> serviceLoader = ServiceLoader.load(HelloSPI.class);
// 执行不同厂商的业务实现,具体根据业务需求配置
for (HelloSPI helloSPI : serviceLoader) {
helloSPI.sayHello();
}
}
}
输出结果如下:
Image Hello
Text Hello
这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
JDBC DriverManager框架对SPI的应用
最初的时候,我们是通过Class.forName(“com.mysql.jdbc.Driver”)来加载数据库相关的驱动,然后获取连接等操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。
JDBC接口定义
首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
mysql实现
在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
postgresql实现
同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。
源码实现
在DriverManager中有一个静态代码块如下:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//使用SPI的ServiceLoader来加载接口的实现
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
上面的代码主要步骤是:
- 从系统变量中获取有关驱动的定义。
- 使用SPI来获取驱动的实现。
- 遍历使用SPI获取到的具体实现,实例化各个实现类。
- 根据第一步获取到的驱动列表来实例化具体实现类。
我们主要关注2,3步,这两步是SPI的用法,首先看第二步,使用SPI来获取驱动的实现,对应的代码是:ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这里没有去META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。
接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:
//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历所有的驱动实现
while(driversIterator.hasNext()) {
driversIterator.next();
}
在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类(ServiceLoader具体的源码实现在下面)。然后是调用driversIterator.next(),此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。
可以看下截图,我在测试项目中添加了两个jar包,mysql-connector-java-6.0.6.jar和postgresql-42.0.0.0.jar,跟踪到DriverManager中之后:
SPI机制的缺陷
通过上面的解析,可以发现,我们使用SPI机制的缺陷:
- 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。