# SPI 机制详解

SPI(Service Provider Interface),是 JDK 内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用

必看视频:10 分钟让你彻底明白 Java SPI,附实例代码演示 #安员外很有码

这个教学视频讲的详略得当,还有代码实战。

从 SPI 机制就可以看出,接口的存在是多么的重要,比如 java.sql.Driver 接口,其他不同厂商可以针对同一接口做出不同的实现,而我们只需要调用接口即可,根本不关心实现是怎么样的,SPI 机制主要思想就是将装配的控制权转移到程序之外。整个学习过程你需要牢记一个词 —— 解耦

# 概念和术语

这部分我就直接照搬上面视频里的内容了,你们看完了有一键三连吗?

  • Sevice:一个公开的接口或抽象类,定义了一个抽象的功能模块。其实就是定义了能够提供哪些行为
  • Service Provider:Service 的实现类,通常是第三方实现的。
  • ServiceLoader:SPI 机制的核心组件,负责在运行时发现并加载 Service Provider。

整个简化的执行流程就是:

# 简单的 SPI 案例

如果你理解了上面的思想,那么我们就来简单的使用下,这部分请务必去 IDEA 尝试一下,相关代码下文可以获取了:

  1. 首先你需要建立这样的文件目录结构

注意,spi-demo 是父项目,如果不会用 IDEA 建立父子工程,给个传送门 ,创建好后,我来解释一下,company 项目是我们实际运行的项目,一个正常的,有 main 的项目。api 项目你可以当成是一个门面框架,你将会在 company 项目中用到它,但是 api 项目只有接口,没有实现,也就是定义了对应的行为,将实现交给其他厂商、社区等实现,你可以类比 jdbc。两个 provider 项目就是实现接口的项目,你可以类比 mysql 厂商实现 jdbc 和 Oracle 厂商实现 jdbc,我们要使用哪个,就导入哪个的 jar 包。

几个重要的设计点:

  • 两个 provider 要实现 api 项目的接口,就要导入 api 的依赖。
  • SPI 的配置文件要放在 META-INF/services 目录下,配置文件名为 service 接口的全限定名,文件内容是 Service Provider 类的全类限定名,多个 Service Provide 用多行表示。

  • 实现类必须要有无参构造函数。

我们现在想用 api 这个项目(框架),那么就要导入这个依赖,同时,想用 poviderA 提供的实现,也要导入它的依赖,然后在运行项目里面编写代码:

public class Application {
    public static void main(String[] args) {
        ServiceLoader<ActionService> loader = ServiceLoader.load(ActionService.class);
        for(ActionService provider : loader) {
            provider.sendMessage("慕青の迷途");
        }
    }
}
/**
结果:
A 方式发送消息:慕青の迷途
A 扩展模块发送消息:慕青の迷途
*/

相关代码放到阿里云盘上了:https://www.aliyundrive.com/s/b5CUyFZ16gX

解释一下:

ServiceLoader.load(Search.class) 在加载某接口时,会去 META-INF/services 下找接口的全限定名文件,再根据里面的内容加载相应的实现类。

这就是 SPI 的思想,接口的实现由 provider 实现,provider 只用在提交的 jar 包里的 META-INF/services 下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。

# SPI 机制应用

SPI 机制应用十分广泛,之前就提到过,多用于框架开发

# JDBC DriverManager

如果你用过旧版本的 JDBC(4.0 之前,jdbc4.0 随 jdk1.6 一起发布),那么连接数据库时通常会如此加载数据库相关驱动:

Class.forName("com.mysql.jdbc.Driver")

但是之后就不用了,这得益于 SPI 机制。

基于内容的完整性,这里给出 jdbc 的操作,你可以自己改一下 url,user,password 以及 sql 语句来适配你的数据库:

public class Application {
    public static void main(String[] args) throws SQLException {
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/study?serverTimezone=Asia/Shanghai&useSSL=false",
                                                "root", "123456");
        String sql = "select username from users";
        ResultSet set = conn.createStatement().executeQuery(sql);
        while(set.next()) {
            System.out.println(set.getString("username"));
        }
    }
}

mysql 的依赖,上面的代码可以直接执行,不需要加载驱动

  • JDBC 接口定义:jdk 本来就有 jdbc 定义,在 java.sql.* 包下,但是没有具体的实现,这些实现由不同厂商提供。你可以看看 java.sql.Driver 定义了哪些方法。
  • mysql 实现:导入依赖,前文已经给出,然后直接看 mysql-connector-java-x.x.x.jar 下有个 META-INF 包。

  • 同理,你可以导入其他数据库厂商实现的 jdbc 依赖,比如 postgresql。

# 源码实现

我们现在不需要使用 Class.forName("com.mysql.jdbc.Driver") 加载驱动了,直接就可以使用 DriverManager 获取连接。但是现在我们并没有看到 SPI 的使用,

我们希望通过 SPI 机制加载驱动(也就是 Driver 的实现),这个肯定不由厂商决定,所以猜都猜得到应该在 java.sql.* 包下找。

关于驱动的查找其实都在 DriverManager 中, DriverManager 是 Java 中的实现,用来获取数据库连接,在 DriverManager 中有一个静态代码块如下:

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

很明显,加载 DriverManager 时,就会执行 loadInitialDrivers 方法:

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;
        }
    });
    // 此时 registeredDrivers 已经有 Driver 实现类了
    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 获取到的具体实现,实例化各个实现类。
  • 根据第一步获取到的驱动列表来实例化具体实现类。

解释一下关键代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这里没有去 META-INF/services 目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。

// 获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 遍历所有的驱动实现
while(driversIterator.hasNext()) {
    driversIterator.next();
}

在遍历的时候,首先调用 driversIterator.hasNext() 方法,这里会搜索 classpath 下以及 jar 包中所有的 META-INF/services 目录下的 java.sql.Driver 文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类

然后是调用 driversIterator.next(); 方法,此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。

看一下 Driver 实现类的源码:

package com.mysql.cj.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!");
        }
    }
}

可以看到当加载 Driver 类时,执行静态代码块就已经将实现类的实例化对象注册到 DriverManager 里面了。

# SPI 实现原理

ServiceLoader 源码有 200 + 行代码,有时间可以看一下:

//ServiceLoader 实现了 Iterable 接口,可以遍历所有的服务实现者
public final class ServiceLoader<S>
    implements Iterable<S>
{
    // 查找配置文件的目录
    private static final String PREFIX = "META-INF/services/";
    // 表示要被加载的服务的类或接口
    private final Class<S> service;
    // 这个 ClassLoader 用来定位,加载,实例化服务提供者
    private final ClassLoader loader;
    // 访问控制上下文
    private final AccessControlContext acc;
    // 缓存已经被实例化的服务提供者,按照实例化的顺序存储
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 迭代器
    private LazyIterator lookupIterator;
    // 重新加载,就相当于重新创建 ServiceLoader 了,用于新的服务提供者安装到正在运行的 Java 虚拟机中的情况。
    public void reload() {
        // 清空缓存中所有已实例化的服务提供者
        providers.clear();
        // 新建一个迭代器,该迭代器会从头查找和实例化服务提供者
        lookupIterator = new LazyIterator(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();
    }
    // 解析失败处理的方法
    private static void fail(Class<?> service, String msg, Throwable cause)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg,
                                            cause);
    }
    private static void fail(Class<?> service, String msg)
        throws ServiceConfigurationError
    {
        throw new ServiceConfigurationError(service.getName() + ": " + msg);
    }
    private static void fail(Class<?> service, URL u, int line, String msg)
        throws ServiceConfigurationError
    {
        fail(service, u + ":" + line + ": " + msg);
    }
    // 解析服务提供者配置文件中的一行
    // 首先去掉注释校验,然后保存
    // 返回下一行行号
    // 重复的配置项和已经被实例化的配置项不会被保存
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {
        // 读取一行
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        //# 号代表注释行
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }
    // 解析配置文件,解析指定的 url 配置文件
    // 使用 parseLine 方法进行解析,未被实例化的服务提供者会被保存到缓存中去
    private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError
    {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        }
        return names.iterator();
    }
    // 服务提供者查找的迭代器
    private class LazyIterator
        implements Iterator<S>
    {
        Class<S> service;// 服务提供者接口
        ClassLoader loader;// 类加载器
        Enumeration<URL> configs = null;// 保存实现类的 url
        Iterator<String> pending = null;// 保存实现类的全名
        String nextName = null;// 迭代器中下一个实现类的全名
        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }
        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }
        private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            }
            if (!service.isAssignableFrom(c)) {
                fail(service, "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            }
        }
        public boolean hasNext() {
            if (acc == null) {
                return hasNextService();
            } else {
                PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                    public Boolean run() { return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
        public S next() {
            if (acc == null) {
                return nextService();
            } else {
                PrivilegedAction<S> action = new PrivilegedAction<S>() {
                    public S run() { return nextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
    // 获取迭代器
    // 返回遍历服务提供者的迭代器
    // 以懒加载的方式加载可用的服务提供者
    // 懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
    public Iterator<S> iterator() {
        return new Iterator<S>() {
            // 按照实例化顺序返回已经缓存的服务提供者实例
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();
            public boolean hasNext() {
                // 
                if (knownProviders.hasNext())
                    return true;
                return lookupIterator.hasNext();
            }
            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }
            public void remove() {
                throw new UnsupportedOperationException();
            }
        };
    }
    // 为指定的服务使用指定的类加载器来创建一个 ServiceLoader
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    // 使用线程上下文的类加载器来创建 ServiceLoader
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    // 使用扩展类加载器为指定的服务创建 ServiceLoader
    // 只能找到并加载已经安装到当前 Java 虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略
    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        ClassLoader prev = null;
        while (cl != null) {
            prev = cl;
            cl = cl.getParent();
        }
        return ServiceLoader.load(service, prev);
    }
    public String toString() {
        return "java.util.ServiceLoader[" + service.getName() + "]";
    }
}

首先,ServiceLoader 实现了 Iterable 接口,所以它有迭代器的属性,这里主要都是实现了迭代器的 hasNextnext 方法。这里主要都是调用的 lookupIterator 的相应 hasNextnext 方法, lookupIterator 是懒加载迭代器。

其次LazyIterator 中的 hasNext 方法,静态变量 PREFIX 就是 ”META-INF/services/” 目录,这也就是为什么需要在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件。

最后,通过反射方法 Class.forName() 加载类对象,并用 newInstance 方法将类实例化,并把实例化后的类缓存到 providers 对象中,( LinkedHashMap<String,S> 类型)然后返回实例对象。

所以我们可以看到 ServiceLoader 不是实例化以后,就去读取配置文件中的具体实现,并进行实例化。而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用 hasNext 方法的时候会去加载配置文件进行解析,调用 next 方法的时候进行实例化并缓存。

所有的配置文件只会加载一次,服务提供者也只会被实例化一次,重新加载配置文件可使用 reload 方法。

可以看到,如果是第一次执行这段代码:

Iterator<ActionService> iterator = loader.iterator();
while(iterator.hasNext()) {
    iterator.next().sendMessage("慕青の迷途");
}

因为 provider 的实例化是懒加载,所以 LinkedHashMap 每次迭代,size 才 + 1,那么上面迭代器 hashNextnext 走的路径都是 lookupIterator.hasNext()/next()

之后再调用这段代码(如果没有调用 reload ()),就会直接走 if 语句,因为此时 LinkedHashMap 已经缓存了 provider 实例。

# 参考

jdbc 操作

https://pdai.tech/md/java/advanced/java-advanced-spi.html

https://www.bilibili.com/video/BV1RY4y1v7mN/?spm_id_from=333.788&vd_source=5acf5a7b23d28e7633e5a9b381c57c42

https://zhuanlan.zhihu.com/p/305056347