JavaSPI详解
目录
- 一个问题
- 什么是SPI
- API 与 SPI
- 一个简单的例子
- SPI机制的实现
- Java SPI的问题
- 为什么SPI机制打破了双亲委派模型
- 参考资料
一个问题
在项目开发中,经常会使用到数据库驱动,我们连接的数据库可能是MySQL
也有可能是Oracle
,但是不管使用什么数据库都是引入数据库驱动配置相应的地址、用户、密码信息就可以使用而不用修改业务代码。
这是因为在JDK
中提供了一个java.sql.Driver
接口。各个数据库厂商只需要实现这个接口,当我们引入相应驱动,连接数据库的时候,就会使用厂商提供的实现,那么又是如何知道厂商实现的类路径的呢??
什么是SPI
SPI
全名Service Provider interface
,翻译过来就是“服务提供接口”,再说简单就是提供某一个服务的接口, 提供给服务开发者或者服务生产商来进行实现。
Java SPI
是JDK
内置的一种动态加载扩展点的实现。
这个机制在一般的业务代码中很少用到(个人接触到的业务没有用到过),但是再底层框架中却被大量使用,包括JDBC
、Dubbo
、Spring
框架、日志接口中都有用到,不同的是有的使用Java
原生的实现,有的框架则自己实现了一套SPI
机制
API 与 SPI
API
全称Application Programming Interface
, 翻译为“应用程序接口”,指的是应用程序为外部提供服务的接口,这个接口通常由服务提供者自行开发,定义好接口后很少改动。API
与SPI
示意图如图1,图2所示
一般应用(模块)之间通过接口进行通讯,服务提供方提供接口并进行实现后,调用方就可以通过调用这个接口拥有服务提供发提供的能力,这个就是API
当接口是由服务调用方提供,并且由服务提供方进行实现时,服务调用方就可以根据自己的需要选择特定实现,而不用更改业务代码以获取相应的功能,这个就是SPI
一个简单的例子
这个功能是向注册中心注册服务的一个示例(过于简单的示例),
首先定义一个接口Registry
, 这个接口只有一个功能,就是向注册中心注册一个服务,但是我现在并不确定我选的是什么注册中心,于是提供了一个统一的接口,由各个厂商进行实现
package cn.bmilk.chat.spi;
public interface Registry {
void registry(String host, int port);
}
厂商实现好后,需要在其META-INF/services
文件夹下新增一个文件,文件名为该接口的全限定名即:cn.bmilk.chat.spi.Registry
, 内容为接口实现的全限定名,这里我写了两个
cn.bmilk.chat.spi.EurekaRegistry
cn.bmilk.chat.spi.ZookeeperRegistry
两个类的都是空实现,内容如下
@Override
public void registry(String host, int port) {
System.out.println(this + "registry , host = " + host +" port = " + port);
}
下面编写测试主类,通过 ServiceLoader
加载 Registry
实现
public class MainTest {
public static void main(String[] args) {
ServiceLoader<Registry> load = ServiceLoader.load(Registry.class);
Iterator<Registry> iterator = load.iterator();
while (iterator.hasNext()){
Registry registry = iterator.next();
registry.registry("127.0.0.1", 10086);
}
}
}
运行结果
class cn.bmilk.chat.spi.EurekaRegistry
cn.bmilk.chat.spi.EurekaRegistry@12a3a380registry , host = 127.0.0.1 port = 10086
class cn.bmilk.chat.spi.ZookeeperRegistry
cn.bmilk.chat.spi.ZookeeperRegistry@29453f44registry , host = 127.0.0.1 port = 10086
从运行结果中可以看到EurekaRegistry
和ZookeeperRegistry
都被实例化并且生成相应的对象。但是我们全程并没有显示的加载和生成EurekaRegistry
和ZookeeperRegistry
类对象,那么是怎么来的呢?
SPI机制的实现
SPI
机制的核心就是ServiceLoader
类。其主要的属性如下:
// 指出接口配置文件的位置,也就是为什么要在META-INF/services/下创建接口的全限定名文件的原因
private static final String PREFIX = "META-INF/services/";
// 正在被加载的类(接口)的class对象
private final Class<S> service;
// 加载使用的类加载器
private final ClassLoader loader;
// 创建 ServiceLoader 时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存已经加载的实现, 按实例化顺序缓存
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
load()
方法的实现如下:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 使用当前线程的ClassLoader进行加载待加载的实现类
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// load 方法本质是创建一个ServiceLoader对象
return new ServiceLoader<>(service, loader);
}
// 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;
reload();
}
public void reload() {
providers.clear();
// 根据接口类型(父类)和类加载器初始化LazyIterator
lookupIterator = new LazyIterator(service, loader);
}
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
跟踪load()
方法发现其本质是创建了一个ServiceLoader
对象,其共有两个参数,分别是代加载的类父类(接口)Class
类对象和类加载器。在构造方法中完成了两件事,一个是变量赋值,一个是调用reload()
方法。reload()
方法则根据接口类型(父类)和类加载器初始化LazyIterator
当执行ServiceLoader#iterator()
时,会创建java.util.Iterator
匿名内部类实现:
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;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
当执行hasNext()
方法时,会先去providers
查找已经加载的缓存实现,如果不存在,则会继续调用LazyIterator#hasNext()
用于发现尚未加载的实现,最后的实现在LazyIterator#hasNextService()
中
LazyIterator
的关键属性
// 缓存所有需要查找jar包(文件)路径,
Enumeration<URL> configs = null;
// 缓存所有被查找到的实现类全限定名
Iterator<String> pending = null;
// 迭代器使用,下一个需要被加载的类全限定名
String nextName = null;
hasNextService()
实现核心如下:
// 获取所由需要扫描的包路径
configs = loader.getResources(fullName);
// 循环扫描configs中所有的包路径,解析META-INF/services中的指定文件(上例中的cn.bmilk.chat.spi.Registry文件)
//
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// pending缓存了所有查找到的类全限定名
pending = parse(service, configs.nextElement());
}
在知道是否存在接口的实现后,就是通过next()
方法获取实现,核心功能由nextService()
贡献,核心实现如下:
// 获取一个实现类全限定名
String cn = nextName;
// 加载这个类
Class<?> c = Class.forName(cn, false, loader);
// 使用反射创建对象
c.newInstance()
hasNextService()
完成堆配置文件的读取,nextService()
完成类的加载和对象的创建,这个一切都没有在ServiceLoader
创建时完成,这也是体现了延迟Lazy
的一个含义
load()与loadInstalled()
loadInstalled()
和load()
一样,本质都是创建了一个ServiceLoader
d对象,不同点是使用的加载器不同,load()
使用的是Thread.currentThread().getContextClassLoader()
当前线程的上下文加载器, loadInstalled()
使用的是ExtClassLoader
加载器来加载
具体实现如下:
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
使用这个方法将只扫描JDK
安装目录jre/lib/ext
下的jar
包中指定的实现,我们应用程序类路径下的实现将会被忽略掉
Java SPI的问题
Java SPI
虽然使用了懒加载机制,但是其获取一个实现时,需要使用迭代器循环加载所有的实现类- 当需要某一个实现类时,需要通过循环一遍来获取
这个两个问题,在Dubbo
实现自己的SPI
机制时进行了增强,可以仅加载自己想要的扩展实现。
为什么SPI机制打破了双亲委派模型 ??
想不明白 说不清楚,想明白再补充
参考资料
JavaSPI详解的更多相关文章
- Linq之旅:Linq入门详解(Linq to Objects)
示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...
- 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)
一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...
- EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解
前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...
- Java 字符串格式化详解
Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...
- Android Notification 详解(一)——基本操作
Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...
- Android Notification 详解——基本操作
Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...
- Git初探--笔记整理和Git命令详解
几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...
- Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)
Android XML shape 标签使用详解 一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...
- Node.js npm 详解
一.npm简介 安装npm请阅读我之前的文章Hello Node中npm安装那一部分,不过只介绍了linux平台,如果是其它平台,有前辈写了更加详细的介绍. npm的全称:Node Package M ...
随机推荐
- 急如闪电快如风,彩虹女神跃长空,Go语言高性能Web框架Iris项目实战-初始化项目ep00
在Golang Web编程的世界里,君不言高性能则已,言高性能必称Iris.彩虹女神的名号响彻寰宇.名动江湖,单论一个快字,无人能出其右,就连以简洁轻量著称于世的Gin也难以望其项背,只见彩虹女神Ir ...
- 100行代码实现一个RISC-V架构下的多线程管理框架
1. 摘要 本文将基于RISC-V架构和qemu仿真器实现一个简单的多线程调度和管理框架, 旨在通过简单的代码阐明如何实现线程的上下文保存和切换, 线程的调度并非本文的重点, 故线程调度模块只是简单地 ...
- Unity3D学习笔记12——渲染纹理
目录 1. 概述 2. 详论 3. 问题 1. 概述 在文章<Unity3D学习笔记11--后处理>中论述了后处理是帧缓存(Framebuffer)技术实现之一:而另外一个帧缓存技术实现就 ...
- java方法---递归
java方法递归 1.什么是递归 就是自己调用自己: 2.递归结构的两个部分 递归头:什么时候不调用自身方法.如果仅有头,将陷入死循环.递归体:什么时候需要调用自身方法.
- windows下Inno Setup打包
基于inno setup的windos打包,主要脚本语言inno script.下载地址:https://jrsoftware.org/isdl.php相关打包教程:https://blog.csdn ...
- Python 爬取网站数据
一.使用request库实现批量下载HTML 二.使用BeautifulSoup库实现html解析 官网:https://beautifulsoup.readthedocs.io/zh_CN/v4.4 ...
- Linux常用基础命令一
一.目录操作 进入路径 cd [目录地址] 切换回主目录 cd 返回上一个路径 cd - 打印当前路径 pwd 列出目录下文件 ls ---查看只包含非隐藏文件 ls -a -----查看目录下所有文 ...
- KingbaseES 缺少库文件问题
在工作中大家经常会遇到找不到某个so 的问题,这类可能是so文件缺失,或者是由于LD_LIBRARY_PATH 环境变量设置不当的原因. 1.库文件 我们通常把一些公用函数制作成函数库,供其它程序使用 ...
- 字符类数据类型和oracle字符类型的区别
为兼容Oracle的数据类型,KingbaseES扩展了Oracle的NUMBER.VARCHAR2.CHAR(n)和DATE类型.该措施使得移植Oracle的Create Table等DDL语句时, ...
- git stash总结
git stash 1. git stash save "message" 执行存储,并添加备注信息(直接git stash 也可以,但没有备注信息) 2. git stash ...