暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

架构修炼之道—让框架更优雅的SPI机制

看点有用的 2021-07-26
1980

    最近在阅读《架构修炼之道》,觉得里面的内容干货满满,都是经过实践总结出来的经验,包括各个方面的相关技术和方案,于是决定对里面的内容做一个系列分享给大家,今天是我们的系列之一:SPI机制。


一、API和SPI区别

1.1 API

    为了做一个简单区别,先来说一下API。对于API,在日常开发工作中我们已经耳熟能详了,但要给它下一个定义,似乎又不是简单的事情。我们直接去认识它的英文全称Application Programming Interface更容易理解:应用编程接口,根据定义拆解,是一个接口,基于应用,且是可编程的。目前一般企业架构内部都会有一个RPC框架,如果没有自研的也都在使用开源的比如比较有名的Dubbo。业务需求确定之后,技术侧也会相互沟通需要对方提供几个API接口,从面向接口编程的角度而言,这里的接口即API(图1)。

图1

    另外还有当前的开放平台也是把企业内部的接口通过API网关暴露出去提供给第三方开发者如图2所示。这两种情况下说的都是API。接口自己定义,自己来实现,然后暴露出去供别人调用,这是属于API概念(图2)。

图2

1.2 SPI

    SPI(Service Provider Interface)  一方制定接口,另外一方提供接口实现,比如在开放平台中,平台制定接口,第三方去实现。这里的第三方是指开放平台的开发者。接口自己定义,但是并不实现,而是由其他模块或系统去实现,这就是SPI

1.3 API和SPI的区别

    API是属于任意的调入,调出方式,这里的任意特指所有接口参数数量,类型都不固定,比如企业内部发布了很多业务的接口,同时很多业务的接口都发布到了API Gateway上从而开放出去。SPI是属于标准化的调入,调出方式,需要有一方来创建接口标准,参数数量和类型固定之后,由第三方来实现。下图3展示了API和SPI的区别。

图3 API和SPI的区别


二、SPI的机制和原理

2.1 SPI的机制

  • Service provider提供Interface的具体实现后,在目录META-INF/services下的文件(以Interface全路径命名)中添加具体实现类的全路径名;

  • 接口实现类的jar包存放在所使用的类加载器能找到的地方。

  • 应用程序使用ServiceLoader动态加载实现类(根据目录META-INF/services下的配置文件找到实现类的全限定名并调用classloader来加载实现类到JVM);

  • SPI的实现类必须具有无参数的构造方法。

  • SPI加载的核心就是ClassLoader的getResource系列方法,jdk提供了一个工具类,就是上面说的ServiceLoader。

2.2 SPI原理

    (1)应用程序调用 ServiceLoader.load 方法,ServiceLoader.load 方法先创建一个新的ServiceLoader,并实例化该类中的成员变数,包括:

  • ClassLoader loader(类载入器)

  • AccessControlContext acc(访问控制器)

  • LinkedHashMap<String, S> providers(用于缓存载入成功的类)

  • LazyIterator lookupIterator(实现迭代器功能)

    (2) 应用程序通过迭代器获取对象实例

ServiceLoader 先判断成员变量 providers 对象中否有缓存实例对象,如果有缓存,直接返回。

如果没有缓存,执行类的装载:读取 META-INF/services/ 下的配置文件,获得所有能被实例化的类的名称,通过反射方法 Class.forName() 载入类对象,并用 instance() 方法将类实例化。把实例化后的类缓存到providers 对象中然后返回实例对象。

下面是SPI原理关键实现ServiceLoader的部分核心源码:

public final class ServiceLoader<S> implements Iterable<S> {
//扫描目录前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
private final Class<S> service;
// 用于定位、加载和实例化实现方实现的类的类加载器
private final ClassLoader loader;
// 上下文对象
private final AccessControlContext acc;
// 按照实例化的顺序缓存已经实例化的类
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;


// 私有内部类,提供对所有的service的类的加载与实例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
//...
private boolean hasNextService() {
if (configs == null) {
try {
//获取目录下所有的类 转换为-URL,工作流的方式获取每一行的数据并截取第一个‘#’之前的数据,并去除前后的空格
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//....
}
}


private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射加载类 将上面获取的url进行转换为对象类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
//实例化
S p = service.cast(c.newInstance());
//放进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}
复制


三、总结

    SPI机制能够在service provider和service user之间实现解耦,在不用修改源码的情况下可以实现扩展,对原来的代码没有侵入性,符合开闭原则。

文章转载自看点有用的,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论