java开发ServiceLoader实现机制及SPI应用
作者:梦想实现家_Z 发布时间:2022-12-24 09:55:05
前言
做过java web开发的小伙伴大多数时候都需要链接数据库,这个时候就需要配置数据库引擎DriverClassName
参数,这样我们的java应用才能通过数据库厂商给的Driver
与指定的数据库建立通信。但是这里就有一个疑问:
java.sql.Driver
是jdk自带的接口,它是由BoostrapClassLoader加载的,DriverClassName
是外部厂商提供的具体实现,是由AppClassLoader加载的,要建立与数据库的通信,必然是通过java.sql.Driver
接口方法发起的,那么在java.sql.Driver
是如何拿到具体实现的呢?它是不是违背了ClassLoader的双亲委派模式呢?
如何绕过双亲委派模式
为了拿到AppClassLoader中加载的java.sql.Driver
实现类,我们可以查看一下DriverManager
是怎么处理的:
public static Driver getDriver(String url)
throws SQLException {
println("DriverManager.getDriver("" + url + "")");
ensureDriversInitialized();
......
}
private static void ensureDriversInitialized() {
......
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 核心代码ServiceLoader
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
......
}
我们最终可以发现,DriverManager
通过ServiceLoader.load(Driver.class)
就拿到了我们配置的DriverClassName
实现类。这就实现在DriverManager
中拿到了外部提供的Driver
实现,绕过来双亲委派模式。
ServiceLoader实现机制
我们来看一下ServiceLoader
是如何实现SPI机制的,先从ServiceLoader.load()
方法入手:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 从当前线程中获取ClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 创建一个ServiceLoader对象
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
1.从当前线程中获取ClassLoader;因为在创建AppClassLoader后,将AppClassLoader设置进当前线程的上下文中;
2.根据ClassLoader以及目标接口类创建一个ServiceLoader对象;
其实ServiceLoader
核心代码在hasNext()
方法中:
@Override
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
最终都会调用到hasNextService()
方法中:
private boolean hasNextService() {
// nextProvider默认为null,如果通过next()取出来了,nextProvider就会变成null
while (nextProvider == null && nextError == null) {
try {
// 找到目标实现类
Class<?> clazz = nextProviderClass();
if (clazz == null)
return false;
if (clazz.getModule().isNamed()) {
// ignore class if in named module
continue;
}
// 判断service接口是否和clazz有父子关系
if (service.isAssignableFrom(clazz)) {
Class<? extends S> type = (Class<? extends S>) clazz;
// 获取无参构造函数
Constructor<? extends S> ctor
= (Constructor<? extends S>)getConstructor(clazz);
// 包装成一个ProviderImpl对象
ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
// 并赋值给nextProvider
nextProvider = (ProviderImpl<T>) p;
} else {
fail(service, clazz.getName() + " not a subtype");
}
} catch (ServiceConfigurationError e) {
nextError = e;
}
}
return true;
}
外部提供的实现类一定要有一个无参构造函数,否则会导致ServiceLoader加载失败;
我们下面再继续深入看看ServiceLoader
是怎么找到实现类的:
private Class<?> nextProviderClass() {
if (configs == null) {
try {
// 拼接文件名:"META-INF/services/接口名称"
// 比如接口名为:java.sql.Driver,
// 那么文件路径就是:"META-INF/services/java.sql.Driver"
String fullName = PREFIX + service.getName();
// 没有指定ClassLoader,就通过getSystemClassLoader()加载目标文件
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// 如果是platformClassLoader,它没有class path,那么看看BootLoader有没有class path
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
// 通过指定classLoader加载目标文件
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 上面代码只会执行一次,这样configs就不会为null,下次进来直接取下一个实现类
// 把configs内容解析成一个迭代器
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
pending = parse(configs.nextElement());
}
// 通过迭代器获取下一个实现类名称
String cn = pending.next();
try {
// 通过类名反射成Class对象
return Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}
1.实现类的载入是因为在META-INF/services/
文件夹中创建了以目标接口名称命名的文件,并在里面写上了实现类的全路径类名。
2.ServiceLoader通过ClassLoader从class path中载入目标文件里面的内容,并解析出实现类的全路径类名;
3.最终通过反射的方式创建出实现类的Class对象,这样就完成了SPI的实现;
SPI在各个框架上的应用
除了在数据库Driver
上使用了SPI
,我们还可以发现SPI
在各个框架上都有大量的应用。比如我最近在看的Seata分布式事务框架,里面就有用到SPI
:io.seata.common.loader.EnhancedServiceLoader
另一个就是我们经常使用的mysql-connector-java以及阿里的Druid:
小结
通过以上源码分析以及示例演示,我们简单做一个小结:
1.ServiceLoader打破双亲委派模式的方式通过获取当前线程上下文中的ClassLoader完成的;
2.SPI
的实现类名称必须放在META-INF/services/
文件夹下面,以目标接口名称作为文件名称,文件内容为目标实现类全路径类名;
3.目标实现类必须要有一个无参构造函数,否则SPI
会失败;
来源:https://juejin.cn/post/7159095184674783239


猜你喜欢
- 一:背景1. 讲故事周五下午运营反馈了一个紧急bug,说客户那边一个信息列表打不开,急需解决,附带的日志文件也发过来了,看了下日志大概是这样
- 今天上班中午吃饱之后、逛博客溜达看到一道题:数组反转 晚上回家洗完澡没事情做,就自己练习一把。public static cla
- 之前写过一篇 Java 线程池的使用介绍文章《线程池全面解析》,全面介绍了什么是线程池、线程池核心类、线程池工作流程、线程池分类、拒绝策略、
- 以下代码实现了android的免提开启和关闭功能需要添加的权限<uses-permission android:name="
- 使用Integer类型查询出现的问题mapper.xml :<select id="count" paramete
- android中的下拉菜单联动应用非常普遍,android中的下拉菜单用Spinner就能实现,以下列子通过简单的代码实现 * 菜单联动。一
- Semaphore、SemaphoreSlim 类两者都可以限制同时访问某一资源或资源池的线程数。这里先不扯理论,我们从案例入手,通过示例代
- json好久没用了,今天在用到json的时候,发现对字符串做解析的时候总是多出双引号。代码如下:string jsonText = &quo
- 网上关于下拉刷新的文章也不少,不过都太长了。恰好发现了官方的下拉刷新库,而且效果还是不错的,简洁美观,用得也挺方便。下面是效果图:我的好友原
- 前言上一篇文章自定义了一个左滑删除的RecyclerView,把view事件分发三个函数dispatchTouchEvent、onInter
- 短8位UUID思想其实借鉴微博短域名的生成方式,但是其重复概率过高,而且每次生成4个,需要随即选取一个。本算法利用62个可打印字符,通过随机
- 父类空间优先于子类对象产生在每次创建子类对象时,先初始化父类空间,再创建其子类对象本身。目的在于子类对象中包含了其对应的父类空间,便可以包含
- 过滤掉其他的播放器,使用我自己的播放器来做 wv.set
- 环境:Spring5.3.12.RELEASE。Spring 3引入了一个core.onvert包,提供一个通用类型转换系统。系统定义了一个
- 这篇文章主要介绍了java的package和import机制原理解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习
- 本文实例讲述了Android中ContextMenu用法。分享给大家供大家参考。具体如下:main.xml文件如下:<?xml ver
- Springboot Aspect切面的实现今天我们来说说spring中的切面Aspect,这是Spring的一大优势。面向切面编程往往让我
- 本文实例为大家分享了C#+EmguCV使用摄像头读取、保存视频的具体代码,供大家参考,具体内容如下在Emgucv中调用摄像头需要用到Vide
- 基本介绍数据回显:模型数据导向视图(模型数据 ---> Controller ---> 视图)说明:SpringMVC在调用方法
- 大家在银行交易某些业务时,都可以看到无论是身份证、银行账号中间部分都是用*号替换的,下面小编把代码整理如下:/// <summary&