目录

  • 一个问题
  • 什么是SPI
  • API 与 SPI
  • 一个简单的例子
  • SPI机制的实现
  • Java SPI的问题
  • 为什么SPI机制打破了双亲委派模型
  • 参考资料

一个问题

在项目开发中,经常会使用到数据库驱动,我们连接的数据库可能是MySQL也有可能是Oracle,但是不管使用什么数据库都是引入数据库驱动配置相应的地址、用户、密码信息就可以使用而不用修改业务代码。

这是因为在JDK中提供了一个java.sql.Driver接口。各个数据库厂商只需要实现这个接口,当我们引入相应驱动,连接数据库的时候,就会使用厂商提供的实现,那么又是如何知道厂商实现的类路径的呢??

什么是SPI

SPI全名Service Provider interface,翻译过来就是“服务提供接口”,再说简单就是提供某一个服务的接口, 提供给服务开发者或者服务生产商来进行实现。

Java SPIJDK内置的一种动态加载扩展点的实现。

这个机制在一般的业务代码中很少用到(个人接触到的业务没有用到过),但是再底层框架中却被大量使用,包括JDBCDubboSpring框架、日志接口中都有用到,不同的是有的使用Java原生的实现,有的框架则自己实现了一套SPI机制

API 与 SPI

API 全称Application Programming Interface, 翻译为“应用程序接口”,指的是应用程序为外部提供服务的接口,这个接口通常由服务提供者自行开发,定义好接口后很少改动。APISPI示意图如图1,图2所示

图1  API示意图

图2  SPI示意图

一般应用(模块)之间通过接口进行通讯,服务提供方提供接口并进行实现后,调用方就可以通过调用这个接口拥有服务提供发提供的能力,这个就是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

从运行结果中可以看到EurekaRegistryZookeeperRegistry都被实例化并且生成相应的对象。但是我们全程并没有显示的加载和生成EurekaRegistryZookeeperRegistry类对象,那么是怎么来的呢?

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()一样,本质都是创建了一个ServiceLoaderd对象,不同点是使用的加载器不同,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详解的更多相关文章

  1. Linq之旅:Linq入门详解(Linq to Objects)

    示例代码下载:Linq之旅:Linq入门详解(Linq to Objects) 本博文详细介绍 .NET 3.5 中引入的重要功能:Language Integrated Query(LINQ,语言集 ...

  2. 架构设计:远程调用服务架构设计及zookeeper技术详解(下篇)

    一.下篇开头的废话 终于开写下篇了,这也是我写远程调用框架的第三篇文章,前两篇都被博客园作为[编辑推荐]的文章,很兴奋哦,嘿嘿~~~~,本人是个很臭美的人,一定得要截图为证: 今天是2014年的第一天 ...

  3. EntityFramework Core 1.1 Add、Attach、Update、Remove方法如何高效使用详解

    前言 我比较喜欢安静,大概和我喜欢研究和琢磨技术原因相关吧,刚好到了元旦节,这几天可以好好学习下EF Core,同时在项目当中用到EF Core,借此机会给予比较深入的理解,这里我们只讲解和EF 6. ...

  4. Java 字符串格式化详解

    Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...

  5. Android Notification 详解(一)——基本操作

    Android Notification 详解(一)--基本操作 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 源码:AndroidDemo/Notification 文中如有纰 ...

  6. Android Notification 详解——基本操作

    Android Notification 详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 前几天项目中有用到 Android 通知相关的内容,索性把 Android Notificatio ...

  7. Git初探--笔记整理和Git命令详解

    几个重要的概念 首先先明确几个概念: WorkPlace : 工作区 Index: 暂存区 Repository: 本地仓库/版本库 Remote: 远程仓库 当在Remote(如Github)上面c ...

  8. Drawable实战解析:Android XML shape 标签使用详解(apk瘦身,减少内存好帮手)

    Android XML shape 标签使用详解   一个android开发者肯定懂得使用 xml 定义一个 Drawable,比如定义一个 rect 或者 circle 作为一个 View 的背景. ...

  9. Node.js npm 详解

    一.npm简介 安装npm请阅读我之前的文章Hello Node中npm安装那一部分,不过只介绍了linux平台,如果是其它平台,有前辈写了更加详细的介绍. npm的全称:Node Package M ...

随机推荐

  1. 10种有用的Linux Bash_Completion 命令示例

    摘要:我们可以对这个 bash 补全进行加速,并使用 complete 命令将其提升到一个新的水平. 本文分享自华为云社区<有用的 Linux Bash_Completion 命令示例(Ster ...

  2. JS判断两个数组的元素是否完全相等

    1.使用ES6 新增的扩展运算符和Set新数据类型判断两个数组是否包含有相同的元素 var arr1 = ['green' , 'yellow' ,'blue' ,'red']; var arr2 = ...

  3. 「学习笔记」单调队列优化dp

    目录 算法 例题 最大子段和 题意 思路 代码 修剪草坪 题意 思路 代码 瑰丽华尔兹 题意 思路 代码 股票交易 题意 思路 代码 算法 使用单调队列优化dp 废话 对与一些dp的转移方程,我们可以 ...

  4. 100 个常见错误「GitHub 热点速览 v.22.35」

    本周的特推非常得延续上周的特点--会玩,向别人家的女朋友发送早安.这个错误是如何发生的呢?如何有效避免呢?自己用 daily_morning 免部署.定制一个早安小助手给女友吧. 除了生活中的错误,工 ...

  5. NetCore性能优化

    NetCore性能优化2.非跟踪查询在只读方案中使用结果时,非跟踪查询十分有用,可以更快速地执行.增加AsNoTracking()表示非跟踪,如:var users = context.User.As ...

  6. Springboot连接数据库

    好家伙, 这里使用的软件是IDEA 2021 1.新建项目 2.更改配置项目文件目录 更改前: 更改后: 2.1.更改pom.xml文件 在该文件中添加: <dependency> < ...

  7. grub2配置文件丢失如何修复

    实验操作准备 此步骤因实验需要所做,系统开机属grub界面无需此步! lsblk命令为了查看根分区挂载在什么位置 rm -rf /boot/grub2/grub.cfg命令为删除grub2配置文件到达 ...

  8. python一招完美搞定Chromedriver的自动更新

    日常的web自动化过程中,我们常常用python selenium库来操纵Chrome浏览器实现网页的自动化.这其中有个比较头疼的问题:Chrome的更新频率非常频繁,与之对应的Chromedrive ...

  9. tar、gzip、zip、jar是什么,怎么查看?

    原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介 如果你是后端程序员,我想你一定见过*.tar.gz.*.zip.*.jar后缀的文件吧,这些都是压缩文件,那这些文 ...

  10. 关于mciSendString函数调用mp3音频的问题

    先说结论:这个函数一定要调用MP3文件,手动改MP3格式无效,一定要保证下载源是MP3格式文件.具体可参考:https://blog.csdn.net/m0_46436640/article/deta ...