之前做项目的时候,用到了LiteFlow规则引擎,LiteFlow中支持了标准关系型结构化数据库的配置源,使用后发现基于这种方式启动项目时LiteFlow会通过以下固定的SQL去加载相关组件
不难发现,这两个SQL加载了同一租户的所有数据,做开发的都知道,我们更多的情况是去加载有效数据(非逻辑删除的数据),虽然在后续的版本中作者也增加了相应的字段来过滤有效数据,但是有的公司数据库通用字段都是统一的(如逻辑删除标志位、创建人信息等等),不满足我们公司的场景,通过阅读源码后发现作者提供了解析器SPI插件接口
,然后通过重写了SQL解析器,可以看到现在的SQL满足了要求
通过该案例,主要是为了引出分享的主要内容:Java SPI机制
大家平时开发的时候有留意的话,就会发现我们使用的框架中有大量使用了java SPI技术(JDBC加载不同类型的数据库驱动、SLF4J加载不同提供商的日志实现类、Spring、Dubbo等等)
SPI (Service Provider Interface)是JDK内置的一种服务提供发现机制
,这种机制通常用于插件架构或模块化系统,使得开发者可以轻松地添加或替换系统中的组件。简单来说,接口的定义(调用方提供)与具体实现是隔离的(服务提供方提供),使用接口的实现类需要依赖某种服务发现机制
。
这里我们不得不提一下API
API (Application Programming Interface) 服务提供方提供的接口与其实现方法
JAVA SPI机制实现
我们平时上下班通过打卡机的时候,打卡机可能是指纹打卡、人脸识别打卡等等。
- 定义接口
java
public interface Clock {
public void clock();
}
- 指纹打卡
java
public class FingerprintClock implements Clock{
@Override
public void clock() {
System.out.println("//// 指纹打卡");
}
}
- 人脸识别打卡
java
public class FaceRecognitionClock implements Clock{
@Override
public void clock() {
System.out.println("//// 人脸识别打卡");
}
}
- 在
resources
下新建META-INF/services/
目录(
能放到别的目录下吗?)
,新建接口全限定名的文件:com.tx.general.spi.Clock
,里面加上需要用到的实现类(我这里配了指纹打卡实现类)
- 使用ServiceLoader.load()加载接口
java
/**
* 通过ServiceLoader获取所有实现了Clock接口的时钟对象,并依次调用它们的clock方法
*/
public static void main(String[] args) {
// 创建ServiceLoader类对象
ServiceLoader<Clock> serviceLoaders = ServiceLoader.load(Clock.class);
// 返回LazyIterator迭代器的匿名迭代器对象
Iterator<Clock> it = serviceLoaders.iterator();
while (it.hasNext()){
Clock clock = it.next();
clock.clock();
}
}
可以看到输出的结果是我们预期的,但是调用方式和平时我们调用方法有所不同,通过ServiceLoader
类使用Java的类加载器机制从META-INF/services/
目录下加载和实例化服务提供者,完成了对方法的调用。
JAVA SPI机制原理
前面提到了通过ServiceLoader
类使用Java的类加载器机制从META-INF/services/
目录下加载和实例化服务提供者,我们主要分析ServiceLoader类
。
load(Class<S> svc, ClassLoader cl)
方法
java
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 创建一个ServiceLoader对象
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// loader如果为空,那么就使用ClassLoader.getSystemClassLoader(),即系统类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
// providers是一个Map结构的链表LinkedHashMap,clear()清空了缓存,这里是什么时候有值的呢?
providers.clear();
// 创建一个Lazy懒加载形式的迭代器
lookupIterator = new LazyIterator(service, loader);
}
该方法创建一个可加载接口服务提供者实例的ServiceLoader类对象,其内部创建一个具有延迟加载功能的迭代器LazyIterator(实现了Iterator迭代器接口
)
iterator()
方法
java
/**
* 返回一个迭代器
*/
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String, S>> knownProviders
= providers.entrySet().iterator();
/**
* 判断是否有下一个元素。
*/
public boolean hasNext() {
// 判断缓存是否为空
if (knownProviders.hasNext())
return true;
// lookupIterator,前文创建的LazyIterator迭代器对象的引用
return lookupIterator.hasNext();
}
/**
* 获取下一个元素。
*/
public S next() {
// 判断缓存是否为空
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// lookupIterator,前文创建的LazyIterator迭代器对象的引用
return lookupIterator.next();
}
};
}
该方法创建一个实现了Iterator接口的匿名内部类实例对象,并返回该实例对象作为一个迭代器。 hasNext()方法调用LazyIterator.hasNextService()
方法,next()方法调用LazyIterator.nextService()
方法
LazyIterator.hasNextService()
方法
java
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
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);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析读取配置文件类名,将读取到的类名存储到ArrayList,包装成iterator返回
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
可以看到在这方法里面我们去读取了META-INF/services/
下的文件,并将service实现类的名字列表返回
LazyIterator.nextService()
方法
java
/**
* 获取下一个服务提供者
* 如果没有下一个服务提供者,则抛出`NoSuchElementException`异常
*
* @return 下一个服务提供者
*/
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 {
// 根据Class对象创建服务提供者的实例
S p = service.cast(c.newInstance());
// 将实例添加到缓存中
providers.put(cn, p);
// 返回服务提供者实例
return p;
} catch (Throwable x) {
// 创建实例失败,抛出异常
fail(service, "Provider " + cn + " could not be instantiated", x);
}
// 不应该执行到这里,抛出`Error`异常
throw new Error();
}
通过反射方法Class.forName()
加载类对象,并用newInstance()
方法将类实例化,把实例化后的类缓存到providers
对象中,到此,我们就搞懂了java SPI的原理
总结
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。但是我们也看到了通过ServiceLoader类
将接口的实现类全部加载并实例化一遍。