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