spi探索-java原生篇

441 阅读6分钟

1.spi是什么

spi的全称是Service provider interface,即服务提供商接口。

在平时的开发中,我们经常接触到概念是api(Application Programming Interface),比如 jdk中hashMap、ArrayList等是java提供给我们的api,我们可以通过读取相关的文档,来学习这些api的使用。业务开发的过程中,后端一般也会提供api给前端,用于数据交互,比较常用的方式就是提供http接口。

那么spi与api有什么关系呢?让我们看一下维基百科上的一段说明:

Service provider interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components.

译文:

SPI是旨在由第三方实现或扩展的 API。 它可用于启用框架扩展和可替换组件。

我们可以理解为spi也是一种api,与上述的两种api的使用场景,最大的不同就是实现交给了第三方去做,对比关系如下图所示:

spi-api对比.png

spi就是一种为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。

2.ServiceLoader的使用

java中的spi主要是通过ServiceLoader来使用的,使用步骤如下:

  1. 定义接口

  2. 实现接口

  3. 实现接口的模块中,resources目录下创建文件:META-INF/services/接口类全名

  4. 通过ServiceLoader.load(接口.class)来获取所有的接口实现类对象

下面通过一个例子来看一下java中原生spi的使用方式(spiDemo (github.com))。

首先定义接口IotDevice,来模拟一个iot设备

public interface IotDevice {
    /**
     * 获取iot设备名称
     *
     * @return 设备名
     */
    String name();

    /**
     * 开机
     */
    void on();

    /**
     * 关机
     */
    void off();

    /**
     * 设备工作中
     */
    void work();
}

然后在其他模块中,创建几个IotDevice的实现类,模拟具体的iot设备

public class XiaomiLight implements IotDevice {
    @Override
    public String name() {
        return "小米米家LED智能台灯1S";
    }

    @Override
    public void on() {
        System.out.println("小爱开灯");
    }

    @Override
    public void off() {
        System.out.println("小爱关灯");
    }

    @Override
    public void work() {
        System.out.println("亮了。。。");
    }
}

在实现类所在模块的resources目录下,创建META-INF/services/top.learningwang.iot.IotDevice文件,内容填写实现类的全名

top.learningwang.iot.device.light.XiaomiLight

然后我们就可以通过ServiceLoader来获取所有IotDevice实现类的对象

public class Run {
    public static void main(String[] args) {
        ServiceLoader<IotDevice> s = ServiceLoader.load(IotDevice.class);
        for (IotDevice iotDevice : s) {
            System.out.println("-------------------");
            System.out.println(iotDevice.name());
            iotDevice.on();
            iotDevice.work();
            iotDevice.off();
            System.out.println("-------------------");
            System.out.print("\n");
        }
    }
}

运行后,可以看到扫描到了所有的IotDevice实现类,并可以通过对象调用接口定义的方法

run.png

3.ServiceLoader源码解析

通过上面的案例,可以看出ServiceLoader是java原生spi实现的核心类,那么它的实现原理是什么呢?为什么必须定义配置文件到META-INF/services/文件夹下面呢,带着这些问题,首先来看一下入口方法 ServiceLoader.load(IotDevice.class)

    // 入口,默认从当前线程中获取类加载器,作为接口实现类的加载器
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

    // 初始化ServiceLoader对象
    public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader) {
        return new ServiceLoader<>(service, loader);
    }

ServiceLoader.load做的只有两件事:

  1. 指定类加载器
  2. 初始化ServiceLoader对象 ServiceLoader的构造方法:
    // 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;

    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    private LazyIterator lookupIterator;

    // 校验class、classLoader,初始化迭代器
    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();
    }

    // 清空类缓存,重新初始化迭代器
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

构造方法中,初始化了类加载器、对象缓存map、迭代器信息。

以上就是ServiceLoader.load方法执行的逻辑,构造迭代器之后,就可以通过循环的方式,来执行迭代逻辑,扫描实现类并创建实现类对象的逻辑就放到了迭代器的实现中。

LazyIterator是ServiceLoader类中的一个私有内部类,构造方法及属性如下

    // 接口类
    Class<S> service;
    // 类加载器
    ClassLoader loader;

    // 迭代过程中用到的属性
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    // 初始化接口类、类加载器信息
    private LazyIterator(Class<S> service, ClassLoader loader) {
        this.service = service;
        this.loader = loader;
    }

迭代器比较核心的方法就是 hasNext、next方法

    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);
        }
    }

具体的实现逻辑,封装到了hasNextService、nextService方法中,先来看一下hasNextService方法

     // 待扫描文件夹前缀
    private static final String PREFIX = "META-INF/services/";
     
    // 扫描 META-INF/services/class
    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }

        if (configs == null) {
            // 扫描jar包,初始化url迭代器
            try {
                // 文件夹前缀 + 类的全名
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }

        while ((pending == null) || !pending.hasNext()) {
            // 读取文件内容
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        // 迭代文件中的每一行,暂存到变量中
        nextName = pending.next();
        return true;
    }

hasNextService方法中扫描resources路径下所有的META-INF/services/类全名文件,并读取文件内容,每次调用遍历一行,并将该行的内容存到了nextName属性中。

下面再来看一下nextService方法做了那些工作

    // 3. 通过反射创建对象,并放入缓存中
    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        // 3.1 nextName在hasNextService已初始化
        String cn = nextName;
        nextName = null;
        // 3.2 反射创建对象,放入缓存中
        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());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                    "Provider " + cn + " could not be instantiated",
                    x);
        }
        throw new Error(); 
    }
}

nextService方法中,使用hasNextService中已赋值的变量nextName,来通过反射的方式来创建对象,并放入到了缓存中。

以上就是jdk中ServiceLoader的具体实现,主要实现思路是通过迭代器去循环读取jar包下指定文件的类全名,并通过反射来创建对象。

4.java源码中spi的应用

java源码中,也有很多用到了spi机制,例如jdbc,首先,回顾一下原生jdbc获取连接的代码写法,以mysql为例

    String url = "jdbc:mysql://localhost:3306/db1";
    String user = "user1";
    String password = "123456";

    // 1.加载驱动程序(jdk1.6之后,不需要再去显式加载JDBC驱动程序)
     Class.forName("com.mysql.jdbc.Driver");
    // 2. 获得数据库连接
    Connection conn = DriverManager.getConnection(url, user, password);

jdk1.6之后,使用ServiceLoader改写了JDBC驱动程序的加载方式,不需要再通过 Class.forName() 去显式加载JDBC驱动程序

public class DriverManager {
    static {
        // 静态加载驱动器
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        // 加载所有lib下的Driver实现
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                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-connector包结构如下

mysql8-connector.png

java.sql.Driver文件中的内容中声明了驱动类 com.mysql.cj.jdbc.Driver,DriverManager会通过spi机制自动加载所有的驱动类。

jdbc通过spi机制实现的好处:

  1. 易于扩展,各厂商按照接口规范实现Driver即可

  2. 扩展一种新型数据库支持,不需要去升级jdk版本

5.总结

spi机制将接口的定义与具体的业务实现分离,而不是和业务端全部耦合再一起,可以运行时根据业务实际场景启用或者替换具体组件,符合高内聚、低耦合的设计原则,能够很好的满足基础框架对外提供扩展能力的需要。

附: 参考资料

Java SPI思想梳理 - 知乎 (zhihu.com)

springboot-starter中的SPI 机制 - 掘金 (juejin.cn)

某厂面试:如何优雅使用 SPI 机制 |周末学习 - 掘金 (juejin.cn)

不懂Java SPI机制,怎么进大厂 - 掘金 (juejin.cn)

设计原则:小议 SPI 和 API - 幸福框架 - 博客园 (cnblogs.com)