SPI 机制使用到很经典的设计原则,在学习之前,首先了解一下:

  • 开闭原则:面向拓展开放,对修改关闭;
  • 里氏替换原则:父类出现的地方都应该可以让子类替换,让子类去增强和扩展功能;
  • 依赖倒置原则:面向接口编程;

SPI 使用

为何需要 SPI ,使用模板设计模式无法解决拓展性问题吗?

  • 使用 SPI 可以简化配置,只需要在外部配置文件中做对应修改就可以
  • 而使用模板模式,一般都是需要在代码中指定加载哪一个子类,配置繁琐;
  • 实现相同功能模块的解耦分离,子类实现以轻量化的插件形式存在
  • 而使用模板模式,一般都是在运行的时候就需要一起编译,实现臃肿;
  • 使用 SPI 可以动态加载和替换外来的功能组件

SPI 的认识

Java SPI 全称 Java Service Provider Interface,是 Java 提供的一种服务提供者发现机制。其核心功能是通过接口找到其实现类。在实际运用中,主要用于在程序启动或运行时,通过 SPI 机制,加载并装配接口实现类,实现组件的替换和动态扩展

典型场景

在我们使用 MySQLOracle 数据库时,只需要引入 MySQL 驱动 jar 包或 Oracle 驱动 jar 包就可以了,底层就是采用 SPI 的方式进行驱动实现的热加载

好处:

  • JDK 的数据库连接操作和驱动的实现彻底解耦,各个厂商只需要关注自己的实现;
  • 使用方不用加载所有数据库驱动,实现插件化
  • 使用方几乎不需要什么配置,就可以切换数据库驱动

如何使用

Java SPI 的核心实现类是 ServiceLoader,使用方式也比较简单,先调用 ServiceLoader.load 加载实现类,然后遍历获取实现类

//第一步:调用ServiceLoader.load加载实现类
ServiceLoader<IProtocol> protocols = ServiceLoader.load(IProtocol.class);
//第二步:通过遍历获取实现类
Iterator<IProtocol> iterator = protocols.iterator();
while (iterator.hasNext()){
    IProtocol protocol = iterator.next();
    System.out.println(protocol);
}

// 要求把IProtocol接口的实现类,写在META-INF/service/目录下,以IProtocol全限定类名命名文件
// 文件内容为子类的完整类地址

SPI 原理

重要属性

  • ServiceLoader 加载配置文件的路径是固定的,为 META-INF/services/
  • 很多时候,创建一个 ServiceLoader 并不一定会进行加载,所以 SPI 实现加载子类是懒加载,即在真正进行子类的迭代遍历时,才会一边去对配置文件 IO ,读取子类,以 Class.forName 的形式,并且不会进行类初始化
  • SPI 加载类有做缓存处理,即已经加载过的子类不会在同个 ServiceLoad 下不会再进行加载,而是直接使用缓存
  • 缓存的内容是子类实现以及对应的实例
  • 即使配置文件发生了变更,也不会再次触发加载,即一个 ServiceLoader 加载是快照式的
  • 不过 ServiceLoader 是支持刷新的,即将上次 load 的内容全部丢弃,当前的 ServiceLoader 作为新的使用
public final class ServiceLoader<S> implements Iterable<S>{
private static final String PREFIX = "META-INF/services/";

    // 被加载的类或接口,即协议组件实例中的IProtocol
    private final Class<S> service;

    // 实现类的类加载器,默认为调用load方法的线程的上下文类加载器
    private final ClassLoader loader;

    // 访问控制上下文,访问控制上下文,这里主要用于控制加载实现类的访问权限。
    private final AccessControlContext acc;

    // 实例化后的实现类,key=实现类全限定名,value=实现类实例
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // ServiceLoader为了支持可遍历的功能,实现的支持简单的懒加载功能的迭代器。
    private LazyIterator lookupIterator;
  
    ......
}

SeriviceLoad::load()

这个方式并不会做任何加载,而是创建一个 ServiceLoader 返回,并且默认是采用当前的系统类加载器进行加载,也支持指定类加载器对接口实现进行 SPI 发现

public static <S> ServiceLoader<S> load(Class<S> service) {
    //获取类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    //调用load方法,传入类加载器
    return ServiceLoader.load(service, cl);
}
// 构造ServiceLoader
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;
    //重新加载,1、清空已加载的类,2、初始化LazyIterator
    reload();
}

public void reload() {
    //清空缓存的已加载的实现类实例
    providers.clear();
    //初始化懒加载迭代器
    lookupIterator = new LazyIterator(service, loader);
}

一边迭代,一边加载

Java SPI 正是在遍历过程中实现的,实现类的解析、加载和实例化

/*
 * 以下是ServiceLoader的iterator方法实现, 返回一个借助懒加载迭代器实现的迭代器
 */
public Iterator<S> iterator() {
    //直接实例化并返回了一个Iterator
    return new Iterator<S>() {
        //实例化遍历器时,将ServiceLoader已经实例化的实现类赋值给了成员变量knownProviders。
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
            
        //iterator.hasNext()会调用这个方法,判断是否还有实现类
        public boolean hasNext() {
            //先判断已加载的实现类中是否存在,存在的话直接返回true
            if (knownProviders.hasNext())
                return true;
            //如果不存在,则调用ServiceLoader中的lookupIterator,看是否存在。
            return lookupIterator.hasNext();
        }

        //iterator.next()会调用这个方法,获取下一个实现类
        public S next() {
            //如果已加载的实现类中存在,则返回已加载的实现类
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            //否则,调用ServiceLoader中的lookupIterator,获取下一个实现类。
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

懒加载器定义

  • ServiceLoader 借助懒加载器,实现遍历时一边加载
  • 它在首次调用 hasNext 时,加载所有的 SPI 配置文件,保存在 configs 中,在首次调用 next 或者 hasNextnext 的时候,解析出所有的子类全限定类名,保存在 pending
  • 每次进行 next 或者 hasNext 就更新 nextName 属性,它是下一个被加载和创建实例的子类;
  • 用这种方式实现子类的按需加载
  • 被加载过的子类会连同该类的实例被缓存起来,保存在一个 LinedHashMap
private class LazyIterator
        implements Iterator<S>
    {
    	// 接口定义
        Class<S> service;
    	// 加载子类的类加载器
        ClassLoader loader;
    	// SPI配置文件
        Enumeration<URL> configs = null;
    	// 等待被加载的子类全限定名称
    	// 在首次调用的hasNext的时候赋值
        Iterator<String> pending = null;
    	// 下一个需要被加载的子类实现全限定名
    	// 每次加载完一个之后或者调用hasNext之后就进行赋值
        String nextName = null;
    ....
}

写到最后的话

Java SPI 机制虽然很强大,但是还是存在一些缺陷,比如

  • 不支持依赖注入,这在我们做框架顶层逻辑抽象的时候,特别是接入 Spring IOC 时就会陷入一些困境,需要去额外补充编码;
  • 还有就是获取实例方式单一,不支持按 key 获取等。

而这些在 dubbo 实现的 SPI 中就提出一些解决方案,它是另外一套更加强大的服务发现机制,等我下次有空的时候再好后说一说吧。