SPI全名Service Provider Interface(服务提供者接口),SPI的主要目的是实现服务的热插拔效果,主要应对的场景是设计者提供一个接口,这个接口的具体实现由不同厂商提供,设计者只要引入厂商提供的实现jar包就可以自动装配,无需改动代码。例如:JDBC的驱动加载。SPI机制满足:
- 不需要改动源码就可以实现扩展,解耦。
- 实现扩展对原来的代码几乎没有侵入性。
- 只需要添加配置就可以实现扩展,符合开闭原则。
其实很多知名框架的自动装配都是借鉴了JDK的SPI机制如SpringBoot的starter、dubbo的spi扩展等。
- 接下来就借助JDBC驱动装置这个经典例子来讲解spi机制。
// 加载驱动
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
Connection connection = DriverManager.getConnection(url, user, paw);
// 创建preparedStatement
// ..省略
JDBC的连接数据库的步骤一般如上段代码所示,由于在第一行代码中显示的加载了mysql驱动这里体现不出spi机制的存在,试问如果我们去掉Class.forName(“com.mysql.jdbc.Driver”);这段代码,当我们获取连接时系统自动去加载数据库驱动可不可行,答案是可行的。那问题来了系统是怎么知道mysql驱动的实现类是com.mysql.jdbc.Driver并且加载他的呢?
DriverManager中有个静态代码块,在DriverManager初始化时会被调用
static {
// 基于SPI机制加载数据库驱动
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;
}
// 根据spi机制装配驱动
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 创建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;
}
});
// spi机制未装配到驱动,获取系统参数中的驱动进行装配
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);
}
}
}
- 创建ServiceLoader服务加载器,在内部设置类加载器为线程上下文类加载器(这个很关键,由于ServiceLoader是由系统类加载器加载的类,所以无法加载classpath下的资源和驱动实现类所以需要打破双亲委派机制借助线程上下文类加载器加载)
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 如果线程上下文类加载器是空,就取系统类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
- 1中遍历全部驱动调用了driversIterator.hasNext(),最终会调用ServiceLoader$LazyIterator.hasNextService()方法,此时会在会获取META-INF/services/java.sql.Driver下的驱动实现类全限定名。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 拼接为 META-INF/services/java.sql.Driver
String fullName = PREFIX + service.getName();
// 如果线程上下文类加载器为null则用系统类加载器,如果不为null则用线程上下文类加载器
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 解析classpath下面的所有的META-INF/services/java.sql.Driver文件获取实现类全限定名
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析核心方法
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
每个厂商的驱动jar都在META-INF/services/java.sql.Driver里存放驱动的实现类
- 1中遍历驱动调用了driversIterator.next();加载驱动。
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 {
// 强转成Driver类型
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated: " + x,
x);
}
throw new Error(); // This cannot happen
}
- com.mysql.jdbc.Driver中有静态方法调用DriverManager.registerDriver()方法将自身注入到DriverManager中用于后续连接获取。
package com.mysql.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
后面获取连接的细节就不多说了,有兴趣的可以自己去撸源码,撸源码的时候也着重关注下ServiceLoader类这个类很多小细节。
总结下SPI实现服务热拔插的关键:
- 约定固定目录META-INF/services/+接口名文件存放接口实现类全限定名
- 打破双亲委派机制使用线程上下文类加载器来加载classpath下面的供应商服务实现(如果线程上下文类加载器设置为自定义的类加载器则可以从数据库、网络等多种途径装配供应商的服务实现)