一、前言
Service Provider Interface 简称SPI,是一种服务发现机制。
本篇博客中,笔者将讲述Java中的SPI机制和Spring框架中的SPI机制、练手demo和其在源码中的应用、优缺点。
二、Java SPI
2.1 原理
Java SPI 是通过Java的类加载机制和反射机制来实现的,具体来说是通过java.util包下的ServiceLoader类中的静态方法<S> ServiceLoader<S> load(Class<S> service)来加载服务,核心逻辑如下:
(1)检查类路径META-INF/services目录下是否存在以接口全限定名命名的文件。
(2)若存在,使用流的方式读取并解析文件内容,获取实现该接口的类的全限定名,并通过Class.forName(cn, false, loader) 方法加载对应的类。
(3)在加载完类之后,会调用service.cast(c.newInstance())方法,通过反射机制创建对应类的实例,并将其缓存到名为LinkedHashMap<String,S> providers的map中。
【注意】当调用<S> ServiceLoader<S> load(Class<S> service)时,并不会立刻将所有实现了该接口的类都一次性加载进来,而是返回一个名为lookupIterator的懒迭代器,只有遍历迭代器时,才会按需加载对应的类及其实例。
2.1.1 源码解析
首先我们宏观了解下ServiceLoad类的结构,其实现了迭代器接口,声明了一些成员变量。
public final class ServiceLoader<S> implements Iterable<S> {
// Java SPI 规定的接口存储位置,会从这个目录下读取内容
private static final String PREFIX = "META-INF/services/";
// 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;
}
接着,调用load()方法,会实列化ServiceLoad和该类的成员变量,最重要的是其调用reload()方法实例化了一个懒迭代器lookupIterator。
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 实例化
return new ServiceLoader<>(service, loader);
}
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;
// 获取懒加载器,并清空服务缓存map
reload();
}
public void reload() {
providers.clear();
// 实例化`懒`迭代器
lookupIterator = new LazyIterator(service, loader);
}
load()方法执行完毕后,并没有加载服务。因为Java SPI实现了懒加载机制,当我们遍历load()方法返回后服务对象列表后,才会加载服务。【注意:因为ServiceLoader类实现了Iterable接口,当遍历时,首先会调用hasNext()方法,最终会调到hasNextService()方法,hasNextService()方法主要获取实现类的全限定名,nextService()方法通过类加载机制和反射机制来实例化获取到的实现类。】
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// fullName = /META-INF/services/全限定名
String fullName = PREFIX + service.getName();
// 转化为URL对象
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;
}
// 读取并解析URL对象
pending = parse(service, configs.nextElement());
}
// 拿到第一个实现类的全限定名
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 通过类加载器获取类的Class对象
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 {
// 通过反射机制获取实例化实现类, 并放入map集合,返回实例化的对象
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(); // This cannot happen
}
至此,Java SPI的核心源码已经解析完毕,接下来,笔者将举个例子使读者更直观的理解SPI
2.2 demo
首先demo的项目结构如下,共有5个module。
hello-spi-interface模块定义了一个接口SaveData,方法为save(String)。
hello-spi-mysql模块实现了该方法,并在META/INF的service目录下创建了名为org.hello.interface.SavaData的文件,文件内容为SaveData接口的实现类的全限定名:org.hello.mysql.MysqlSave。hello-spi-redis和hello-spi-pgsl实现原理同hello-spi-mysql一样。
hello-spi-app模块通过pom.xml文件来按需引入。例如,这里我们引入了mysql和pgsql模块,因此当服app服务启动时,其运行了mysql和pgsl的保存数据的执行逻辑。
2.3 源码中的应用
2.4 优缺点
三、Spring SPI
2.1 原理
2.2 demo
2.3 源码中的应用
2.4 优缺点
四、扩展
SPI在Dubbo框架中的应用```