前言
本文主要针对SPI工作机制的学习作记录,顺便分享下如何使用spi实现插件需求
1、SPI是什么?
SPI全称Service Provider Interface,我理解下就是一套服务发现机制,即为某个接口寻找它的实现类,整体机制图如下:
系统里抽象的各个模块,往往有不同的实现方案。在面向对象设计中,一般推荐模块之间基于接口进行编程,模块之间不对实现类硬编码,一旦代码里涉及了具体实现,就违反了可拔插原则,如果需要替换一种实现,就需要修改源代码,这很不可取。为了使模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
SPI应运而生,它就是一种为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,所以SPI的核心思想就是解耦。
看到这里是不是发现SPI不就是面向接口思想的具体应用嘛,是的就是如此,举个通俗易懂的例子:
sun公司在起初时只有一组jdbc类,那么后来这么多数据库厂商该如何实现相同的功能?这就依赖SPI机制了,sun公司开发了一组jdbc接口,只要每个数据库厂商均实现该接口,并按规定的SPI标准打包,最后再放到classpath下就ok啦,没有一点代码的侵入性
2、SPI的常见使用场景
准确来说,SPI适用于:调用者根据实际需求启用、扩展、或者替换框架的实现策略
2.1、如上文所说,数据库驱动加载实现类的加载,例:jdbc加载不同类型数据库的驱动
2.2、日志门面接口实现类加载,例:sla4j加载不同提供商的日志实现类
2.3、Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
2.4、Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
3、SPI的使用约定
要是有SPI机制需满足如下约定:
当服务提供者提供了某个接口的具体实现后,需在jar包的resource/META-INF/services目录下创建一个以“接口全限定名”为命名的文件,文件里填上该实现类的全路径。这样当外部程序装配这个模块的时候,就能通过该jar包中META-INF/services下的配置找到具体的实现类,并装载实例化,完成模块的注入。
基于这样一个约定,外部程序能就能很好的找到服务接口的实现类,而无需在代码里指定,jdk提供服务实现查找的工具类为:
java.util.ServiceLoader
4、SPI的具体使用demo
4.1、首先定义标准接口,如下:
public interface People {
void say();
}
4.2、然后基于该接口,各服务提供者提供不同的实现,如:
public class Boy implements People {
@Override
public void say() {
System.out.println("i'm a boy");
}
}
public class Girl implements People {
@Override
public void say() {
System.out.println("i'm a girl");
}
}
4.3、提供者需在resouce目录下新建META-INF/services目录,然后新建一个以“接口全限定名”的文件,文件中填上该接口的具体的实现类,每个实现占一行,如下:
文件中内容如下:
impl.Girl
impl.Boy
4.4、外部程序装配时通过ServiceLoad装载类进行装载,如下:
public void test01() {
ServiceLoader<People> serviceLoader = ServiceLoader.load(People.class);
serviceLoader.forEach(People::say);
}
4.5、最后输出结果如下:
i'm a girl
i'm a boy
Process finished with exit code 0
5、SPI源码解读
主要就是针对ServiceLoad的源码进行分析,代码量很少看起来很容易,首先看下该类的各成员变量:
public final class ServiceLoader<S> implements Iterable<S> {
// 即SPI约定路径
private static final String PREFIX = "META-INF/services/";
// 目标接口
private final Class<S> service;
// 指定类加载器
private final ClassLoader loader;
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器,具体加载过程就在该类中实现
private LazyIterator lookupIterator;
demo中使用的是ServiceLoader.load(Class<S> service)方法,未传入特定的classLoader,稍后讲插件时会用到另外一个重载方法,那么我们先看下源码:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
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;
reload();
}
应用程序通过迭代器接口获取对象实例时先判断ServiceLoader中的缓存providers是否有缓存实例对象,如果有则直接返回,如果没有则需执行类的加载,执行过程如下:
1、读取META-INF/services/目录下的配置文件,获取所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,我们的插件demo就是基于此特性,此处的加载代码如下:
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
2、通过反射方法Class.forName()加载类对象,然后通过instance()方法实例化对象,因此实现类必须拥有一个无参构造方法,反射知识请走:反射和注解总结
3、把实例化后的类放进缓存map中,然后返回该类
6、使用SPI实现扩展插件小demo
有了上文的引入,不知各位对实现插件是不是心中已经有了想法?该需求正是基于ServiceLoader可以跨越jar包获取配置文件的特性,通过在加载时传入特定的classLoader来实现,那么问题即为如何构造该类加载器呢?一般使用到URLClassLoader类来访问外部jar包并封装classLoader,代码大致如下:
// 这里的localPath即为上传后的插件所在的本地服务器地址,针对分布式环境,一般可采用统一上传到mongo,在进行插件解析时下载指定插件并上传到本地
URL url = new URL("file:" + localPath);
// 使用URLClassLoader访问外部jar包并构建classLoader
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url}, Thread.currentThread().getContextClassLoader());
最后将该classLoader作为参数传入load()方法即可,上传下载方法请走:上传文件实现、下载方法实现
7、SPI总结
优点:
使用java的SPI机制可以很好地实现解耦,使得第三方服务模块的装配控制逻辑可以与调用者的业务代码分离,而不是耦合在一起,应用程序可以根据实际业务情况启用框架扩展或替换框架组件
缺点:
1、虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类
2、并发多线程使用ServiceLoader类的实例是不安全的
到这里分享记录就完啦 ~ Winter is coming ! 喜爱的美剧今天终于更新啦,最后一季第一集开刷!