文章目录

  • SPI 机制以及 JDBC 打破双亲委派
    • SPI 机制
      • 简介
      • jdk SPI 原理
      • 写一个 demo
    • JDBC 打破双亲委派模型

文章已收录我的仓库: Java学习笔记

SPI 机制以及 JDBC 打破双亲委派

本文基于 jdk 11

SPI 机制

简介

何为 SPI 机制?

SPI 在Java中的全称为 Service Provider Interface,是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

例如本文的中心 jdbc,我们可以使用统一的接口 Connection 去操控各种数据库,但你有没有想过,难道 jdk 真的内置了所有的数据库驱动吗?

显然这是不可能的,我们平时使用需要我们自己导入对应 jar 包去使用不同的数据库,例如 MySQL、SqlServer 数据库等。可是,我们使用的Connection conn = DriverManager.getConnection(url,user,pass)又是确确实实是 jdk 内置的一个接口,其实这就是一种 SPI 机制,即官方定义好一个接口,由不同的第三方服务者去实现这个接口。

那么问题来了,SPI 机制是如何实现的呢?

答案很简单,遵守官方的约定

jdk SPI 原理

SPI 可以有不同实现,但无论怎样核心思想都是遵守官方规则。官方不一定要是 jdk,例如 SpringBoot 就定义了一套规则,但我们这里仍然讲 jdk 的实现原理。

实现 SPI 机制的关键点在于要知道接口的具体实现类是哪一个,这些实现类是第三方服务提供的,必须得有一种方法让 JVM 知道使用哪个实现类,因此官方定义了如下规则:

  • 服务提供者的类必须要实现官方提供的接口(或继承一个类)。
  • 将该具体实现类的全限定名放置在资源文件下 META-INF/services/${interfaceClassName} 文件中,其中 ${interfaceClassName} 是接口的全限定名。
  • 如果扫描到多个具体实现类,jdk 会初始化所有的这些类。

这就是 jdk 的约定,既然要求服务者提供的类名放在 META-INF/services/${interfaceClassName} 文件下,那么官方肯定要去扫描这个文件,官方提供了一个实现:ServiceLoader.load 方法。

获取具体类实例的伪代码如下:

ServiceLoader load = ServiceLoader.load(XXX.class);
for (XXX x : load) {// o 就是我们要获取的实例,XXX 是一个官方定义的接口接口System.out.println(x);
}

ServiceLoader.load 方法返回一个 ServiceLoader 实例,我们需要通过遍历其迭代器去获取所有可能的实例,核心代码就在迭代器中了。

我们来看看这个迭代器的源码,我们主要看 hasNext 方法,因为 next 方法本身依赖于 hasNext 方法:

public Iterator<S> iterator() {return new Iterator<S>() {int index;@Overridepublic boolean hasNext() {if (index < instantiatedProviders.size())return true;return lookupIterator1.hasNext();}};
}

跳到了 lookupIterator1.hasNext() 方法中,lookupIterator1 是调用 newLookupIterator() 方法返回的,来看看这个方法:

private Iterator<Provider<S>> newLookupIterator() {Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();return new Iterator<Provider<S>>() {@Overridepublic boolean hasNext() {return (first.hasNext() || second.hasNext());}@Overridepublic Provider<S> next() {if (first.hasNext()) {return first.next();} else if (second.hasNext()) {return second.next();} else {throw new NoSuchElementException();}}};
}

可以发现主要有两种加载方式,一种是模块化的加载,另一种是普通的懒加载方式,我们应该会进到 second.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);}
}

second.hasNext() 方法又调用了 hasNextService() 方法,来看看这个方法的逻辑:

private boolean hasNextService() {while (nextProvider == null && nextError == null) {try {Class<?> clazz = nextProviderClass();if (clazz == null)return false;if (service.isAssignableFrom(clazz)) {Class<? extends S> type = (Class<? extends S>) clazz;Constructor<? extends S> ctor  = (Constructor<? extends S>)getConstructor(clazz);ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);nextProvider = (ProviderImpl<T>) p;} } catch (ServiceConfigurationError e) {nextError = e;}}return true;
}

这个方法首先判断 nextProvider 是不是空,如果不是的话说明已经有资源,直接返回;我们是初次加载,肯定为空,因此进入循环,可以看到主要的方法就是 nextProviderClass(),通过这个方法返回一个构造器,然后进行实包装,并设置 nextProvider 等于包装后的 ProviderImpl,有了 ProviderImpl,自然可以通过反射获取实例!

这里核心方法应该是 nextProviderClass() :

static final String PREFIX = "META-INF/services/";
private Class<?> nextProviderClass() {if (configs == null) {// 路径名String fullName = PREFIX + service.getName();// 根据路径名取得资源,这个 loader 是线程上下文取得的configs = loader.getResources(fullName);}// pending 是一个 String 的迭代器while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return null;}pending = parse(configs.nextElement());}String cn = pending.next();try {return Class.forName(cn, false, loader);} catch (ClassNotFoundException x) {return null;}
}

可以看到在这一步通过 PREFIX + service.getName() 获取文件名,这个就是我们说的META-INF/services/${interfaceClassName} 约定的文件,然后取得对应资源,pending 是一个 String 的迭代器,其内就是具体实现类的全限定名,如果这个迭代器存在,说明已经解析过了,则直接返回 Class.forName(pending.next(), false, loader)

如果 pending 迭代器到底了,那么会再一次进入循环,并调用 configs.nextElement() 再次读取,**这也就是说明如果有多个配置文件,那么所有的类都会被加载!**直到真的什么都没有了,那么抛出异常,返回 null,这里返回 null 的话,那么hashNext 方法就会返回 false。

如果是第一次解析,那么会进入到 pending = parse(configs.nextElement()); 方法,来看看这个方法:

private Iterator<String> parse(URL u) {Set<String> names = new LinkedHashSet<>(); // preserve insertion orderURLConnection uc = u.openConnection();uc.setUseCaches(false);try (InputStream in = uc.getInputStream();BufferedReader r = new BufferedReader(new InputStreamReader(in, UTF_8.INSTANCE))) {int lc = 1;while ((lc = parseLine(u, r, lc, names)) >= 0);}return names.iterator();
}

这个方法很简单,就是读取配置中的每一行,添加到 Set 中,然后返回其迭代器。所以我们将返回一个配置文件中所有的全限定名!

这就是 jdk 提供的 SPI 机制的具体实现原理!

注:上述源码经过了些许简化

写一个 demo

假如我们有一个 HelloPrinter 的接口,这个接口需要第三方来提供,则可以这样编写来获取具体实现类:

public class Test {public static void main(String[] args) {ServiceLoader<HelloPrinter> load = ServiceLoader.load(HelloPrinter.class);Iterator<HelloPrinter> iterator = load.iterator();while (iterator.hasNext()) {HelloPrinter helloPrinter = iterator.next();helloPrinter.hello();}}
}

试问,这里有没有出现任何关于具体实现类的信息?没有!具体实现类对客户来说是完全透明的,客户只知道 HelloPrinter 这个接口!现在什么都没做,这个代码什么也不会输出。

然后我们启动另一个项目写两个实现类 HelloPrinterImpl1 和 HelloPrinterImpl2,按照约定建立如下目录:

在这个文件中填写实现者的全限定名:

com.demo.HelloPrinterImpl1
com.demo.HelloPrinterImpl2

然后 maven 打包,让客户端引入这个 jar 包,重新运行,现在客户端的输出是:

我是第一个实现者!
我是第二个实现者!

JDBC 打破双亲委派模型

本节前置知识:类加载机制

JDBC 其实也是一种 SPI 机制,例如当我们引入 MYSQL 驱动的 jar 包时:

可以发现这与我们上面讲的一模一样,由此可见我们使用的驱动应该是 “com.mysql.cj.jdbc.Driver” 这个类。

当引入了 jar 包之后,则可以通过代码:

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/xxx?serverTimezone=GMT", "root", "123456");

来获取数据库连接,Connection 是官方的接口,无论什么数据库都可以一样操作,非常方便。

但我们为什么说 jdbc 打破了双亲委派模型呢?

原因在于 DriverManager.getConnection 方法其实是加载了 com.mysql.cj.jdbc.Driver 这个类,然后这个驱动去创建连接。

问题是 DriverManager 是属于 jdk 的官方类,应当是由引导类加载器(jdk8 以前叫启动类加载器)加载的,而 com.mysql.cj.jdbc.Driver 这个类明显不能由引导类加载器加载,而是由应用类加载器(也叫系统类加载器)加载。

public class Test {public static void main(String[] args) throws Exception {System.out.println(DriverManager.class.getClassLoader().getName());Class c = Class.forName("com.mysql.cj.jdbc.Driver");System.out.println(c.getClassLoader().getName());}
}

输出是:

platform
app

可见确实是由平台类加载器和应用类加载器去加载这两个类,问题是 DriverManager 要如何加载 com.mysql.cj.jdbc.Driver 驱动,直接使用 Class.forName 可行吗?

不可行!因为 Class.forName 默认会采用调用者自己的类加载器去加载,DriverManager 的类加载器是平台类加载器,显然加载不到驱动类。

所以 DriveManager 必须要使用次一级的类加载器去加载,这里是使用了 SPI 机制,在 getConnection 方法中,我们调用了一行代码:

private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {// 省略ensureDriversInitialized();// 省略return driver.connect(url, info);
}

ensureDriversInitialized() 这个方法:

private static void ensureDriversInitialized() {synchronized (lockForInitDrivers) {if (driversInitialized) {return;}String drivers;AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);Iterator<Driver> driversIterator = loadedDrivers.iterator();while (driversIterator.hasNext()) {driversIterator.next();}return null;}});driversInitialized = true;}
}

可以看到在这个方法内调用了 loadedDrivers = ServiceLoader.load(Driver.class) 方法,然后遍历迭代器,driversIterator.next() 相当于实例化了 Drive,别看它好像啥也没做,但事实上在实例化的过程中,触发了 Drive 类的初始化语句块的调用:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}static {try {DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}}
}

它向 DriverManager 注册了自己!因此 DriverManager 可以保存这个 Driver!

ServiceLoader 加载实现原理上面已经说了,但我故意没讲 ServiceLoader.load 方法:

@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}

load 方法内,设置默认的加载器为线程上下文加载器,这个线程上下文类加载器在上一篇文章已经详细说了,就不再赘述了。

由于我们在主线程调用的 DriverManager.getConnection 方法,现在已经很明显了,ServiceLoader 使用了主线程的类加载器去加载,这当然是应用加载器!

现在再来总结一下为什么说 jdbc 打破了双亲委派,我认为有两点:

  1. DriverManager 数据 jdk 官方类,使用平台加载器,然而却使用了应用加载器去加载 Driver 类,这相当于是高层次的类使用了低层次的类加载器,这算是逆向打通了双亲委派模型。
  2. 另外就是 ServiceLoader 使用线程上下文加载器直接获得类加载器,而没有一层一层向上调用,这肯定也是违反了双亲委派模型的。

下文加载器,这个线程上下文类加载器在上一篇文章已经详细说了,就不再赘述了。

由于我们在主线程调用的 DriverManager.getConnection 方法,现在已经很明显了,ServiceLoader 使用了主线程的类加载器去加载,这当然是应用加载器!

现在再来总结一下为什么说 jdbc 打破了双亲委派,我认为有两点:

  1. DriverManager 数据 jdk 官方类,使用平台加载器,然而却使用了应用加载器去加载 Driver 类,这相当于是高层次的类使用了低层次的类加载器,这算是逆向打通了双亲委派模型。
  2. 另外就是 ServiceLoader 使用线程上下文加载器直接获得类加载器,而没有一层一层向上调用,这肯定也是违反了双亲委派模型的。

SPI 机制以及 jdbc 打破双亲委派相关推荐

  1. 【04-JVM面试专题-什么是双亲委派机制(父类委托机制)?如何打破双亲委派机制?双亲委派机制的优缺点?什么是沙箱安全机制呢?】

    什么是双亲委派机制?如何打破双亲委派机制? JVM的双亲委派机制知道吗?怎么打破它呢?你看看自己掌握的怎么样呢? 什么是双亲委派机制?(父类委托机制) 检查某个类是否已经加载 自底向上,从Custom ...

  2. 双亲委派机制及打破双亲委派示例

    双亲委派机制 在加载类的时候,会一级一级向上委托,判断是否已经加载,从自定义类加载器->应用类加载器->扩展类加载器->启动类加载器,如果到最后都没有加载这个类,则回去加载自己的类. ...

  3. 证明SPI打破双亲委派

    1.什么是双亲委派? 注:此处直接摘抄周志明老师的<深入理解java虚拟机> ​ 站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap Class ...

  4. 【JVM】Java类的加载流程以及双亲委派,全盘托管,以及如何打破双亲委派机制

    JVM基础生命周期流程图 只有main()方法的java程序执行流程 classLoader.loadClass()的类加载流程(除引导类,所有类都一样) 加载:通过IO查找读取磁盘上的字节码文件,在 ...

  5. JVM类加载机制、双亲委派机制、自定义类加载器、打破双亲委派机制

    1.类加载器 站在Java虚拟机的角度看,只有两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机.JDK8中), ...

  6. JVM系列(三):打破双亲委派及案例

    打破双亲委派机制 上一章我们讲到了类加载器和双亲委派机制的一些原理,对于双亲委派机制,我们也了解了双亲委派机制有沙箱安全机制和避免类的重复加载两大优点,这一章我们来讲述为什么要打破双亲委派机制以及如何 ...

  7. JVM17_Tomcat打破双亲委派机制、执行顺序、底层代码原理、Tomcat|JDBC破坏双亲委派机制带来的面试题

    文章目录 ①. Tomcat类加载机制 ②. Tomcat执行顺序 ③. ClassLoader的创建 ④. ClassLoader加载过程 ⑤. Tomcat破坏双亲委派机制带来的面试题 ①. To ...

  8. 如何打破双亲委派机制

    双亲委派机制 第一次知道何为打破双亲委派机制是通过阅读周志明的<深入理解Java虚拟机>,我们知道双亲委派机制是指当一个类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器 ...

  9. 怎么打破双亲委派机制

    Java类加载器 Bootstrap ClassLoader:根类加载器,负责加载java的核心类,它不是java.lang.ClassLoader的子类,而是由JVM自身实现: Extension ...

最新文章

  1. RouterOS限速更高级运用
  2. oracle可以迁徙mysql吗_项目oracle迁徙到mysql的小总结
  3. java两个很大的数相加_两个超大数的相加
  4. 深度学习下的点击率预测:交互与建模
  5. 安卓入门系列-01开发工具Android Studio的安装
  6. 【通俗易懂】理解Python中的if __name__ == ‘__main__‘
  7. 电商产品评论数据情感分析代码详解
  8. eclipse中junit_在Eclipse中有效使用JUnit
  9. 怎么判断一个字符串的最长回文子串是否在头尾_回文自动机入门
  10. 《从问题到程序:用Python学编程和计算》——1.2 Python语言简介
  11. PHP数组的使用方法小结
  12. eclipse安装spring boot插件spring tool suite
  13. php libiconv close_PHP出现undefined reference to `libiconv' 错误的解决方法
  14. NetMeeting不能共享桌面的解决办法
  15. 使用prettier统一编码风格
  16. 第1章 【蓦然回首】开篇引导【少年,奋斗吧】
  17. 单片机C语言九个重要的知识点总结
  18. 老梁说天下——慈善的红与黑
  19. cad安装日志文件发生错误_CAD 2012 安装出错,错误log文件如下,该怎么修复 在线等。...
  20. 数据挖掘简介(摘自维基百科)

热门文章

  1. 校园导游图C语言数据结构,用C语言和数据结构中的无向图存储结构编一个校园导游图完全的程序代码.docx...
  2. 深度学习机器学习面试题——自然语言处理NLP,transformer,BERT,RNN,LSTM
  3. EDIUS校正颜色轮的教程
  4. excel坐标在Autocad展点线 第1篇
  5. 仓库管理系统、WMS、仓储管理、入库、出库、移库、调拨、报损、盘点、采购、退货、业务管理、销售、财务记账、应收、应付、库存清单、库存预警、库存容量、库存台账、库位管理、产品管理、承运商、供应商、权限
  6. 06-测试用例设计方法-场景法
  7. 2022年注册安全工程师安全生产专业实务(建设施工安全)考试模拟试题卷及答案
  8. visio多树枝直角加箭头_零基础国画教程:树枝基本画法详解,简单易学,小白也能学会!...
  9. PHP短信通知+语音播报自动双呼解决方案
  10. 如何使用阿里巴巴矢量图标库下载使用字体图标?