天天看点

Java SPI实现插件化写到最后的话

SPI

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

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

SPI

使用

为何需要

SPI

,使用模板设计模式无法解决拓展性问题吗?
  • 使用

    SPI

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

    SPI

    可以动态加载和替换外来的功能组件;

SPI

的认识

Java SPI

全称

Java Service Provider Interface

,是

Java

提供的一种服务提供者发现机制。其核心功能是通过接口找到其实现类。在实际运用中,主要用于在程序启动或运行时,通过

SPI

机制,加载并装配接口实现类,实现组件的替换和动态扩展。

典型场景

在我们使用

MySQL

Oracle

数据库时,只需要引入

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

    或者

    hasNext

    next

    的时候,解析出所有的子类全限定类名,保存在

    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

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