JDK中的SPI机制

前言

最近学习类加载的过程中,了解到JDK提供给我们的一个可扩展的接口:java.util.ServiceLoader
之前自己不了解这个机制,甚是惭愧...

什么是SPI

SPI全称为(Service Provider Interface),是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。

首先放个图:我们在“调用方”和“实现方”之间需要引入“接口”,可以思考一下什么情况应该把接口放入调用方,什么时候可以把接口归为实现方。

先来看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:
1.是对实现的说明(我可以给你提供什么)
2.组织上位于实现方所在的包中
3.实现和接口在一个包中

当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:
1.是对实现的约束(要提供这个功能,实现者需要做那些事情)
2.组织上位于调用方所在的包中
3.实现位于独立的包中(也可认为在提供方中)

简而言之
API会告诉您特定的类/方法为您执行什么操作,而SPI则告诉您必须执行哪些操作才能符合要求。通常,API和SPI是分开的。例如,在JDBC中,Driver类是SPI的一部分:如果只想使用JDBC,则不需要直接使用它,但是实现JDBC驱动程序的每个人都必须实现该类。但是,有时它们会重叠。Connection接口既是SPI,又是API:您在使用JDBC驱动程序时通常会使用它,并且需要由JDBC驱动程序的开发人员来实现。

JDK SPI使用说明及示例

要使用SPI比较简单,只需要按照以下几个步骤操作即可:

1.在jar包的META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名
2.接口实现类所在的jar包在classpath下
3.主程序通过java.util.ServiceLoader动态状态实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
4.SPI的实现类必须带一个无参构造方法

举例1

下例是使用maven引入了mysql的依赖后执行的,MySQL驱动内的截图:

java.sql.Driver文件全部内容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
public class SpiTest {

    public static void main(String[] args) {
        ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = loader.iterator();
        while(iterator.hasNext()) {
            Driver driver = iterator.next();
            System.out.println("driver is " + driver.getClass() + ", classLoader is " + driver.getClass().getClassLoader());
        }
        System.out.println("当前上下文类加载器是:" + Thread.currentThread().getContextClassLoader());
        System.out.println("ServiceLoader的类加载器是:" + ServiceLoader.class.getClassLoader());
    }
}

运行结果:

driver is class com.mysql.jdbc.Driver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
driver is class com.mysql.fabric.jdbc.FabricMySQLDriver, classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
当前上下文类加载器是:sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader的类加载器是:null

SPI相关的类加载的逻辑

因为ServiceLoader位于java.util.ServiceLoader,所以这个类是会被启动类加载器所加载,然后我们分析一下ServiceLoader.load(Driver.class)的源码。
根据类加载的原理:如果一个类由类加载器A加载,那么这个类的依赖类也会被类加载器A加载(前提是这个依赖类尚未被加载过)。
当执行ServiceLoader.load(Driver.class),如果不使用线程上下文类加载器来打破双亲委托模型,那么该方法的关联类也会被启动类加载器加载。

333    public static <S> ServiceLoader<S> load(Class<S> service) {
334        ClassLoader cl = Thread.currentThread().getContextClassLoader();
335        return ServiceLoader.load(service, cl);
336    }

所以我们看到334行获取了线程上下文类加载器,然后调用另一个重载的load方法去加载Driver(即所谓的Service)。
继续跟踪ServiceLoader.load(service, cl)的代码,会发现它依次执行了如下动作:
1.初始化了一个:ServiceLoader对象,new ServiceLoader<>(service, loader)
2.执行reload()
3.new LazyIterator(service, loader)
这里所有的loader都是线程上下文类加载器,它默认为系统类加载器。

这时,SpiTest中的ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);就执行完了,
跟着执行SpiTest中的Iterator<Driver> iterator = loader.iterator();
这里当执行iterator.hasNext()的时候,就会进入到刚才初始化的LazyIterator类中,执行其中的下列方法,
所以这也是为什么"在jar包的META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名"

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    //这里的PREFIX是一个常量:META-INF/services/
                    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;
        }

仔细阅读内部类LazyIterator类的源码,就可以知道:
1.JDK是怎么读取META-INF/services目录下的内容
2.JDK是怎么加载这些SPI的类的

JDK SPI的不足

JDK SPI的使用很简单。也做到了基本的加载扩展点的功能。但JDK SPI有以下的不足:
1.需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。
2.配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
3.扩展如果依赖其他的扩展,做不到自动注入和装配
4.不提供类似于Spring的IOC和AOP功能
5.扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的JDK SPI不支持

posted @ 2020-10-08 10:27  可苦可乐  阅读(734)  评论(0编辑  收藏  举报