乐趣区

深入理解SPI机制

一、什么是 SPI
SPI,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。

这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。我们先通过一个很简单的例子来看下它是怎么用的。

1、小栗子
首先,我们需要定义一个接口,SPIService

package com.viewscenes.netsupervisor.spi;
public interface SPIService {

void execute();

}
然后,定义两个实现类,没别的意思,只输入一句话。

package com.viewscenes.netsupervisor.spi;
public class SpiImpl1 implements SPIService{

public void execute() {System.out.println("SpiImpl1.execute()");
}

}
———————- 我是乖巧的分割线 ———————-
package com.viewscenes.netsupervisor.spi;
public class SpiImpl2 implements SPIService{

public void execute() {System.out.println("SpiImpl2.execute()");
}

}
最后呢,要在 ClassPath 路径下配置添加一个文件。文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。
文件路径如下:

SPI 配置文件位置

内容就是实现类的全限定类名:

com.viewscenes.netsupervisor.spi.SpiImpl1
com.viewscenes.netsupervisor.spi.SpiImpl2
2、测试
然后我们就可以通过 ServiceLoader.load 或者 Service.providers 方法拿到实现类的实例。其中,Service.providers 包位于 sun.misc.Service,而 ServiceLoader.load 包位于 java.util.ServiceLoader。

public class Test {

public static void main(String[] args) {Iterator<SPIService> providers = Service.providers(SPIService.class);
    ServiceLoader<SPIService> load = ServiceLoader.load(SPIService.class);

    while(providers.hasNext()) {SPIService ser = providers.next();
        ser.execute();}
    System.out.println("--------------------------------");
    Iterator<SPIService> iterator = load.iterator();
    while(iterator.hasNext()) {SPIService ser = iterator.next();
        ser.execute();}
}

}
两种方式的输出结果是一致的:

SpiImpl1.execute()

SpiImpl2.execute()

SpiImpl1.execute()
SpiImpl2.execute()
二、源码分析
我们看到一个位于 sun.misc 包,一个位于 java.util 包,sun 包下的源码看不到。我们就以 ServiceLoader.load 为例,通过源码看看它里面到底怎么做的。

1、ServiceLoader
首先,我们先来了解下 ServiceLoader,看看它的类结构。

public final class ServiceLoader<S> implements Iterable<S>

// 配置文件的路径
private static final String PREFIX = "META-INF/services/";
// 加载的服务类或接口
private final Class<S> service;
// 已加载的服务类集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 类加载器
private final ClassLoader loader;
// 内部类,真正加载服务类
private LazyIterator lookupIterator;

}
2、Load
load 方法创建了一些属性,重要的是实例化了内部类,LazyIterator。最后返回 ServiceLoader 的实例。

public final class ServiceLoader<S> implements Iterable<S>

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;
    // 先清空
    providers.clear();
    // 实例化内部类 
    LazyIterator lookupIterator = new LazyIterator(service, loader);
}

}
3、查找实现类
查找实现类和创建实现类的过程,都在 LazyIterator 完成。当我们调用 iterator.hasNext 和 iterator.next 方法的时候,实际上调用的都是 LazyIterator 的相应方法。

public Iterator<S> iterator() {

return new Iterator<S>() {public boolean hasNext() {return lookupIterator.hasNext();
    }
    public S next() {return lookupIterator.next();
    }
    .......
};

}
所以,我们重点关注 lookupIterator.hasNext()方法,它最终会调用到 hasNextService。

private class LazyIterator implements Iterator<S>{

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null; 
private boolean hasNextService() {
    // 第二次调用的时候,已经解析完成了,直接返回
    if (nextName != null) {return true;}
    if (configs == null) {
        //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
        //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
        String fullName = PREFIX + service.getName();
        // 将文件路径转成 URL 对象
        configs = loader.getResources(fullName);
    }
    while ((pending == null) || !pending.hasNext()) {
        // 解析 URL 文件对象,读取内容,最后返回
        pending = parse(service, configs.nextElement());
    }
    // 拿到第一个实现类的类名
    nextName = pending.next();
    return true;
}

}
4、创建实例
当然,调用 next 方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。

private class LazyIterator implements Iterator<S>{

private S nextService() {
    // 全限定类名
    String cn = nextName;
    nextName = null;
    // 创建类的 Class 对象
    Class<?> c = Class.forName(cn, false, loader);
    // 通过 newInstance 实例化
    S p = service.cast(c.newInstance());
    // 放入集合,返回实例
    providers.put(cn, p);
    return p; 
}

}
看到这儿,我想已经很清楚了。获取到类的实例,我们自然就可以对它为所欲为了!

三、JDBC 中的应用
我们开头说,SPI 机制为很多框架的扩展提供了可能,其实 JDBC 就应用到了这一机制。回忆一下 JDBC 获取数据库连接的过程。在早期版本中,需要先设置数据库驱动的连接,再通过 DriverManager.getConnection 获取一个 Connection。

String url = “jdbc:mysql:///consult?serverTimezone=UTC”;
String user = “root”;
String password = “root”;

Class.forName(“com.mysql.jdbc.Driver”);
Connection connection = DriverManager.getConnection(url, user, password);
在较新版本中(具体哪个版本,笔者没有验证),设置数据库驱动连接,这一步骤就不再需要,那么它是怎么分辨是哪种数据库的呢?答案就在 SPI。

1、加载
我们把目光回到 DriverManager 类,它在静态代码块里面做了一件比较重要的事。很明显,它已经通过 SPI 机制,把数据库驱动连接初始化了。

public class DriverManager {

static {loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

}
具体过程还得看 loadInitialDrivers,它在里面查找的是 Driver 接口的服务类,所以它的文件路径就是:META-INF/services/java.sql.Driver。

public class DriverManager {

private static void loadInitialDrivers() {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {
            // 很明显,它要加载 Driver 接口的服务类,Driver 接口的包为:java.sql.Driver
            // 所以它要找的就是 META-INF/services/java.sql.Driver 文件
            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;
        }
    });
}

}
那么,这个文件哪里有呢?我们来看 MySQL 的 jar 包,就是这个文件,文件内容为:com.mysql.cj.jdbc.Driver。
MySQL SPI 文件
2、创建实例
上一步已经找到了 MySQL 中的 com.mysql.cj.jdbc.Driver 全限定类名,当调用 next 方法时,就会创建这个类的实例。它就完成了一件事,向 DriverManager 注册自身的实例。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {

static {
    try {
        // 注册
        // 调用 DriverManager 类的注册方法
        // 往 registeredDrivers 集合中加入实例
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {throw new RuntimeException("Can't register driver!");
    }
}
public Driver() throws SQLException {// Required for Class.forName().newInstance()}

}
3、创建 Connection
在 DriverManager.getConnection()方法就是创建连接的地方,它通过循环已注册的数据库驱动程序,调用其 connect 方法,获取连接并返回。

private static Connection getConnection(

    String url, java.util.Properties info, Class<?> caller) throws SQLException {   
//registeredDrivers 中就包含 com.mysql.cj.jdbc.Driver 实例
for(DriverInfo aDriver : registeredDrivers) {if(isDriverAllowed(aDriver.driver, callerCL)) {
        try {
            // 调用 connect 方法创建连接
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {return (con);
            }
        }catch (SQLException ex) {if (reason == null) {reason = ex;}
        }
    } else {println("skipping:" + aDriver.getClass().getName());
    }
}

}
4、再扩展
既然我们知道 JDBC 是这样创建数据库连接的,我们能不能再扩展一下呢?如果我们自己也创建一个 java.sql.Driver 文件,自定义实现类 MyDriver,那么,在获取连接的前后就可以动态修改一些信息。

还是先在项目 ClassPath 下创建文件,文件内容为自定义驱动类 com.viewscenes.netsupervisor.spi.MyDriver

自定义数据库驱动程序
我们的 MyDriver 实现类,继承自 MySQL 中的 NonRegisteringDriver,还要实现 java.sql.Driver 接口。这样,在调用 connect 方法的时候,就会调用到此类,但实际创建的过程还靠 MySQL 完成。

package com.viewscenes.netsupervisor.spi

public class MyDriver extends NonRegisteringDriver implements Driver{

static {
    try {java.sql.DriverManager.registerDriver(new MyDriver());
    } catch (SQLException E) {throw new RuntimeException("Can't register driver!");
    }
}
public MyDriver()throws SQLException {}

public Connection connect(String url, Properties info) throws SQLException {System.out.println("准备创建数据库连接.url:"+url);
    System.out.println("JDBC 配置信息:"+info);
    info.setProperty("user", "root");
    Connection connection =  super.connect(url, info);
    System.out.println("数据库连接创建完成!"+connection.toString());
    return connection;
}

}
——————– 输出结果 ———————
准备创建数据库连接.url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC 配置信息:{user=root, password=root}
数据库连接创建完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

退出移动版