SPI是什么
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
SPI整体机制图如下:
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。
应用场景
SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo,Cipher等等。
SPI流程:
- 定义接口标准
- 第三方提供具体实现: 实现具体方法, 配置 META-INF/services/${interface_name} 文件
- 开发者使用
比如JDBC场景下:
- 首先在Java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商提供。
- 在MySQL的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
- 同样在PostgreSQL的jar包PostgreSQL-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是PostgreSQL对Java的java.sql.Driver的实现。
我们也可以自定义SPI。
示例
先定义一个接口
package org.ifool.spiDemo;
public interface HelloSpi {
public void sayHello();
}
定义两个实现类
HelloImpl1:
package org.ifool.spiDemo;
public class HelloImpl1 implements HelloSpi {
public void sayHello() {
System.out.println("Hello Impl 1");
}
}
HelloImpl2:
package org.ifool.spiDemo;
public class HelloImpl2 implements HelloSpi {
public void sayHello() {
System.out.println("Hello Impl 2");
}
}
在META-INF/services(对于maven工程可在src/main/resources下新建META-INF目录)创建文件,名字就是org.ifool.spiDemo.HelloSpi,然后内容为两个实现类。
内容如下:
org.ifool.spiDemo.HelloImpl1
org.ifool.spiDemo.HelloImpl2
也就是说org.ifool.spiDemo.HelloSpi在这里有两个实现类。
main函数中用ServiceLoader进行加载
package org.ifool.spiDemo;
import java.util.ServiceLoader;
public class App
{
public static void main(String[] args) {
ServiceLoader<HelloSpi> serviceLoader = ServiceLoader.load(HelloSpi.class);
for (HelloSpi helloSPI : serviceLoader) {
helloSPI.sayHello();
}
}
}
运行效果如下:
Hello Impl 1
Hello Impl 2
这个ServiceLoader实现了Iterable。
public final class ServiceLoader<S>
implements Iterable<S>
也就是说,对于一个定义好的SPI接口,如果在类路径下存在这个接口的实现,那么我们就可以把使用ServiceLoader把这个接口的实现类都加载进来,并且每个实现类都放一个实例到这个ServiceLoader,这个实例是通过newInstance()实现的,所以实现类必须有一个无参构造函数。
每个接口,可以有多个实现类,但是我们只能顺序的遍历ServiceLoader来逐个获取,没法通过map.get()使用名字获取一个实现对象,而且这个过程是懒加载的,只有真正遍历的时候才会加载并创建实现类。
假如我们把HelloImpl2修改一下,增加一个有参数的构造函数,就会报错
package org.ifool.spiDemo;
public class HelloImpl2 implements HelloSpi {
public void sayHello() {
System.out.println("Hello Impl 2");
}
public HelloImpl2(int a) {
a = 5;
}
}
再次执行会抛异常:
Hello Impl 1
Exception in thread "main" java.util.ServiceConfigurationError: org.ifool.spiDemo.HelloSpi: Provider org.ifool.spiDemo.HelloImpl2 could not be instantiated
at java.util.ServiceLoader.fail(ServiceLoader.java:232)
这个异常并不是在ServiceLoader.load(HelloSpi.class)时抛的,因为HelloImpl1已经被实例化了,而是在遍历ServiceLoader时抛的异常,说明实现类的加载和创建是lazy模式的。
大多数框架使用ServiceLoader的时候,并不一定需要创建的这个对象,只是需要它做类的加载以及一些初始化工作。下面分析一下是怎么实现的。
代码分析
ServiceLoader这个类不复杂,调用它加载接口的实现类时,它会到各个jar包中的META-INF/services中寻找实现类,使用class.forName加载类,然后用newInstance获得一个实例,再放到Map中,因为这个Map是private的,所以外界没法使用get方法获取实例。
下面是它的成员变量,可以看到META-INF/services是写死在代码里的。
public final class ServiceLoader<S>
implements Iterable<S>
{
//到META-INF/services中搜索相应的ServiceProvider类名
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
//把得到的Provider实例放到一个LinkedHashMap中
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
它用了一个懒加载的策略,
//在调用ServiceLoader.load(HelloSpi.class)的时候,会传入一个ContextClassLoader,然后继续调用ServiceLoader.load(HelloSpi.class,cl)
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
//这个函数是private的,如果cl为空的话,使用systemClassLoader,继续调用reload
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();
}
//reload清空providers,然后新建一个LazyIterator,这个其实只是把实现类的名称记下来了
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
//真正要获取Service实例的时候,才会加载类,并且new一个实例放到providers中
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()); //new一个实例,并且以类名为key放到map里
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
JDBC分析
Java的JDBC Driver接口在rt.jar里,名字是java.sql.Driver,它提供了几个接口,主要的是connect接口,然后具体实现是由厂商实现的。
public interface Driver {
Connection connect(String url, java.util.Properties info)
throws SQLException;
boolean acceptsURL(String url) throws SQLException;
DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;
int getMajorVersion();
int getMinorVersion();
boolean jdbcCompliant();
public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
我们可以看到在mysql-connector里的META-INF/services里有一个文件java.sql.Driver,它的实现是com.mysql.cj.jdbc.Driver
com.mysql.cj.jdbc.Driver
我们使用JDBC的时候,都是从DriverManager开始的,例如下面获取Connection
package org.ifool.spiDemo;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class JDBCDemo {
public static void main(String[] args) throws SQLException {
//Class.forName("com.mysql.cj.jdbc.Driver"); 已经不需要了
String url = "jdbc:mysql://localhost:3306/mysql?serverTimezone=GMT%2B8";
String user = "root";
String password = "123456";
Connection connections = DriverManager.getConnection(url, user, password);
}
}
DriverManager在初始化的时候,在它的static代码块中,会使用ServiceLoader加载所有的java.sql.Driver的实现类,这样各个java.sql.Driver的实现类会被加载,它们的static块也会被执行,同时newInstance创建一个实例,但是这里没用到。
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
/**
先使用老的方式,在jdbc.properties里找提供者,这是为了兼容老版本,再使用ServiceLoader机制**/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
//使用ServiceLoader加载实现类,必须调用iterator.next()才会真正加载,因为是懒加载的,这里的作用只是加载一下类而已,实际没用到初始化的实例,但是类里的static块会被执行
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;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
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;
}
});
假设我们的类路径里有mysql和db2的实现类,那么它们都会被初始化,看一下mysql的实现,它在static块中调用了DriverManager把自己注册到了DriverManager中。同样,DB2可能也会做一些类似的操作。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
我们再看DriverManager的getConnection函数,它会遍历registeredDrivers,选择出Allowed的driver,尝试用这个Driver去connect, 这个过程可能会有用db2的driver去连mysql数据库,但是db2 driver连接的时候,根据url,发现不是db2数据库,则立马返回失败,尝试用下一个driver去连。判断是否Allowed,用的是isDriverAllowed的,这个后面再说。
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
为什么需要在下面的函数里判断⼀下是否允许使用呢?DriverManager管理着JVM⾥所有的Driver,但是同⼀个Driver可能被加载多次,⽐如tomcat⾥,多个应⽤都会加载mysql的driver,但是DriverManager在选择的时候,必须选择与调⽤者的classloader⼀样的Driver。
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}
这个⽅法⽐较特别的地⽅在于,它拿到⼀个Driver,然后判断这个Driver是不是与caller⽤的同⼀个classloader,如果不是的话,那么调⽤forName的时候,正好⼜⽤这个caller的classloader加载了⼀个Driver放到了registeredDrivers⾥⾯,我们看⼀个实例。
在⼀个tomcat⾥,有两个war包,都⽤的同⼀个版本的mysql驱动,tomcat重启的时候,两个war包会有⼀个先调⽤DriverManager.getConnection(),接着另⼀个调⽤。
war1调⽤DriverManager.getConnection()
DriverManager通过SPI机制把所有的jdbc driver都加载⼀次,这时候使⽤的类加载器是war1的,我们记作war1loader,DriverManager通过war1loader加载mysql Driver,mysql Driver主动register⾃⼰,这时候registeredDrivers的结果如下:
registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver"}
接下来,war2调⽤DriverManager.getConnection(),因为Drivermanager已经初始化过了,所以SPI那⼀套流程不会走了。会遍历registeredDrivers,并且判断是否是⾃⼰加载的,war2的加载器为war2loader,在iisDriverAllowed中,会调⽤
class.forName("com.mysql.cj.jdbc.Driver", true, war2loader)
显然这个结果与已经存在的不⼀致,但是,我们⽤war2loader加载驱动,会再次调⽤Driver的初始化,
它继续调⽤register,所以现在的结果就是
registeredDrivers={"war1loader: com.mysql.cj.jdbc.Driver", "war2loader: com.mysql.cj.jdbc.Driver"}
因为registeredDrivers是CopyOnWriteList,循环会继续往下走,下⼀次就能走过isAllowed,然后可以调⽤connect。
Thread Context ClassLoader
前⾯多次出现了ContextClassloader,没有展开解释,ContextClassLoader这个机制不太好理解,我们 先来看⼀下双亲委派机制。
底层的类加载器要加载⼀个类时,先向上委托,有没有发现⼀个特点, 这种双亲委派机制,直接⽗加载器是唯⼀的,所以向上委托,是不会有⼆义性的(OSGI不在讨论范围内)。 但是,假如在上层的类(例如DriverManager,它是由bootstrap classloader加载的)⾥要加载底层的类,它会⽤⾃⼰的加载器去加载,对于SPI来说,它的实现类都是在下层的,需要由下层的classloader加载,
还是以DriverManager为例,假设它在⾃⼰的代码⾥调⽤(虽然没有在代码⾥写上mysql,但是只要把mysql的jar包放在这,Drivermanager最终会扫描到并且调⽤class.forName("com.mysql.c.jdbc.driver")的,只是它传了classloader):
class.forName("com.mysql.cj.jdbc.driver");
我们看forName的代码
@CallerSensitive
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
此处会寻找caller的类,然后找它的classloader,DriverManager调⽤的forName,所以此处的caller就是DriverManager.class,但是我们知道DriverManager是bootstrap加载的,那此处获取classloader就是null。forName0是native⽅法,它发现classloader是null就尝试⽤bootstrap加载,但是我们要加载的是mysql的类,bootstrap肯定是不能加载的。
假设我们的委派链是个单纯的单链表,那么我们⽤⼀个双向链表向下委托就⾏了,但是这种机制的委托链并不是单链表,所以向下委托是有⼆义性的。
那怎么办呢?谁调⽤我,我就⽤谁的加载器,这个加载器放在哪呢,就跟线程绑定,也就是Thread Context ClassLoader。
所以DriverManager在实际调⽤forName的时候,要⽤ContextClassLoader。 它⼀共有两处会加载类
⼀处是类初始化调⽤ServiceLoader的时候,我们知道ServiceLoader使⽤的是contextClassloader。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
⼀处是getConnection的时候,先检查⼀下caller的classloader,如果是null的话就使⽤ContextClassloader,在isDriverAllowed⾥加载类
// Worker method called by the public getConnection() methods.
private static Connection getConnection( String url, java.util.Properties info, Class<?> caller) throws
SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
.....
if(isDriverAllowed(aDriver.driver, callerCL)) {
.....
Thread Context ClassLoader意义就是:⽗Classloader可以使⽤当前线程Thread.currentthread().getContextLoader()中指定的classloader中加载的类。颠覆了⽗ClassLoader不能使⽤⼦Classloader或者是其它没有直接⽗⼦关系的Classloader中加载的类这种情况。这个就是Thread Context ClassLoader的意义。⼀个线程的默认ContextClassLoader是继承⽗线程的,可以调⽤set重新 设置,如果在main线程⾥查看,它就是AppClassLoader。