当前位置: 首页 > article >正文

以mysql驱动为案例,从源码角度深入分析Java的SPI机制

本文将以mysql驱动为案例,深入跟踪源码分析Java的SPI(Service Provider Interface)机制。

环境

java 8,mysql8.0,mysql-connector-java 8.0.20

代码

public class MysqlConnectorTest {public static void main(String[] args) {Connection conn = null;try {Class.forName("com.mysql.cj.jdbc.Driver");conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/xxx", "name", "password");// 业务代码} catch (Exception e) {e.printStackTrace();} finally {if (conn != null) {try {conn.close();} catch (SQLException e) {e.printStackTrace();}}}}
}

分析

在上述代码中,Class.forName("com.mysql.cj.jdbc.Driver");即使去掉依然可以成功运行,这正是因为jdk利用其SPI机制替我们执行了这行代码。假设没有SPI机制,当我们需要切换数据源驱动时,例如从mysql切到oracle,此时不仅要导入oracle的驱动依赖包,还要修改这一行代码。而借助SPI机制,我们可以省去这行代码,从而只需引入依赖包即可实现目的,这便是SPI机制解耦作用的直观体现。

首先分析这行代码为什么需要执行(注意是执行,而不一定是由我们的业务代码触发),这行代码的意思也很简单,即加载指定类com.mysql.cj.jdbc.Driver,而类加载期间是能执行代码的(静态变量赋值、静态代码块等),故查看该类的源码:

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.registerDriver(new Driver());,望文生义得知其目的是向DriverManager类中添加mysql驱动,进一步跟踪源码(期间经过了方法重载):

public static synchronized void registerDriver(java.sql.Driver driver,DriverAction da)throws SQLException {/* Register the driver if it has not already been added to our list */if(driver != null) {registeredDrivers.addIfAbsent(new DriverInfo(driver, da));} else {// This is for compatibility with the original DriverManagerthrow new NullPointerException();}println("registerDriver: " + driver);}

关键代码registeredDrivers.addIfAbsent(new DriverInfo(driver, da));将驱动(即com.mysql.cj.jdbc.Driver的实例对象)添加到列表registeredDrivers中,供后续获取数据库连接使用。故代码Class.forName("com.mysql.cj.jdbc.Driver");的目的就是创建一个mysql数据库驱动对象,并将其添加到DriverManager类维护的驱动列表中。

接下来分析当我们移除该行代码时,SPI机制是如何帮我们执行的。首先,java.sql.DriverManager并非核心基础类,不会由jvm在启动时加载,而是当业务代码执行到conn = DriverManager.getConnection("url", "name", "password");时,才会去加载DriverManager类,进一步跟踪源码:

static {loadInitialDrivers();println("JDBC DriverManager initialized");
}private static void loadInitialDrivers() {
// 省去其他代码AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {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类加载时,执行了loadInitialDrivers方法,观察核心代码ServiceLoader.load(Driver.class); ,注意此处的Driver并非com.mysql.cj.jdbc.Driver,而是java.sql.Driver,前者是位于jar包mysql-connector-java中的类,后者是位于jdk的rt.jar中的接口,二者是实现的关系,切勿混淆!同样望文生义得知这行代码的目的是加载驱动类,进一步跟踪源码:

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader) {return 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();lookupIterator = new LazyIterator(service, loader);
}private class LazyIteratorimplements Iterator<S> {private LazyIterator(Class<S> service, ClassLoader loader) {this.service = service;this.loader = loader;}
}

链路有点长,但仔细观察得知其全程并没有执行形如Class.forName(xxx)的代码,只是将传入的java.sql.Driver接口的Class对象与当前线程的上下文类加载器存在了ServiceLoader中,并在其内部类LazyIterator中同样拷贝了二者的引用。LazyIterator直译懒惰的迭代器,也即懒加载思想,只有当使用到该类时是再去加载它。

回到DriverManager类的loadInitialDrivers方法,观察核心代码 while(driversIterator.hasNext()) { driversIterator.next(); },这里的迭代器driversIterator是ServiceLoader类对Iterator接口的实现,本质上是套了个壳子,对已加载的服务提供者(即mysql驱动类的实例)做了缓存,最后依然调用的是LazyIterator中的hasNext()next()方法,进一步跟踪LazyIterator的hasNext()源码:

public boolean hasNext() {if (acc == null) {return hasNextService();} else {PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {public Boolean run() { return hasNextService(); }};return AccessController.doPrivileged(action, acc);}
}private boolean hasNextService() {if (nextName != null) {return true;}if (configs == null) {try {String fullName = PREFIX + service.getName();if (loader == null)configs = ClassLoader.getSystemResources(fullName);elseconfigs = loader.getResources(fullName);} catch (IOException x) {fail(service, "Error locating configuration files", x);}}while ((pending == null) || !pending.hasNext()) {if (!configs.hasMoreElements()) {return false;}pending = parse(service, configs.nextElement());}nextName = pending.next();return true;
}

首次进来时configs、pending、nextName都是空,故会执行configs = loader.getResources(fullName);逻辑,而String fullName = PREFIX + service.getName();,PREFIX为静态常量,其值为"META-INF/services/",service.getName()即为字符串“java.sql.Driver”,此处获取了文件“META-INF/services/java.sql.Driver”的URL,并通过后续代码pending = parse(service, configs.nextElement());读取文件内容,pending即为该文件的内容,它是一个字符串迭代器(单行文本为一个元素),这意味着我们可以在该文件中配置多个数据库驱动(以换行分隔)。

这里的代码决定了JAVA SPI机制的使用方法:在ClassPath下新建"META-INF/services/"目录,并在其中新建文件,文件名为接口的全限定类名,文件内容为接口实现类的全限定类名,多个实现类以换行分隔。按这种方式配置后,我们就可以在代码中通过ServiceLoader.load(Xxx.class)的方式加载这些实现类,但请注意其懒加载机制,只有真正遍历该方法返回的迭代器时才会执行类加载!

进一步跟踪ServiceLoader的迭代器源码:

public S next() {if (knownProviders.hasNext())return knownProviders.next().getValue();// lookupIterator为内部类LazyIterator的实例return lookupIterator.next();
}// 内部类LazyIterator的next方法
public S next() {if (acc == null) {return nextService();} else {PrivilegedAction<S> action = new PrivilegedAction<S>() {public S run() { return nextService(); }};return AccessController.doPrivileged(action, acc);}
}// 内部类LazyIterator的nextService方法
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?> c = null;try {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 {S p = service.cast(c.newInstance());providers.put(cn, p);return p;} catch (Throwable x) {fail(service,"Provider " + cn + " could not be instantiated",x);}throw new Error();          // This cannot happen
}

拨开云雾见光明,终于看到了Class.forName(cn, false, loader);,这里的cn即为文件“META-INF/services/java.sql.Driver”中的一行内容,并随着迭代的进行不断变更(见hasNextService方法),在mysql驱动中即为“com.mysql.cj.jdbc.Driver”。同时,代码service.cast(c.newInstance());新建了com.mysql.cj.jdbc.Driver的实例对象,并将其向上转型为java.sql.Driver对象缓存起来。

经过上述的源码分析,整体的流程已经变得清晰起来。若业务代码不主动加载数据库驱动,当执行到DriverManager.getConnection(xxx)时,会先去触发DriverManager的类加载,然后在这个过程中利用SPI机制加载java.sql.Driver的实现类com.mysql.cj.jdbc.Driver(涉及到读取META-INF/services/java.sql.Driver文件),最后在com.mysql.cj.jdbc.Driver的类加载过程中向DriverManager注册mysql驱动实例。

至此,当DriverManager完成类加载时,它已经以静态变量的方式拥有了一个或多个数据库驱动实例,从而供后续获取数据库连接使用。例如通过DriverManager获取连接的源码:

@CallerSensitive
public static Connection getConnection(String url,String user, String password) throws SQLException {java.util.Properties info = new java.util.Properties();if (user != null) {info.put("user", user);}if (password != null) {info.put("password", password);}return (getConnection(url, info, Reflection.getCallerClass()));
}private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {// 省去其他代码for(DriverInfo aDriver : registeredDrivers) {// If the caller does not have permission to load the driver then// skip it.if(isDriverAllowed(aDriver.driver, callerCL)) {try {println("    trying " + aDriver.driver.getClass().getName());Connection con = aDriver.driver.connect(url, info);if (con != null) {// Success!println("getConnection returning " + aDriver.driver.getClass().getName());return (con);}} catch (SQLException ex) {if (reason == null) {reason = ex;}}} else {println("    skipping: " + aDriver.getClass().getName());}}// 省去其他代码
}

其中,registeredDrivers即为在类加载期间利用SPI机制注册的数据库驱动列表。通过这里的源码可知,DriverManager在获取数据库连接时,会遍历所有的数据库驱动,将数据库配置信息传入这些驱动程序,直到成功获取连接为止。

扩展

上述源码中还有一个细节值得注意,ServiceLoader使用的是线程上下文类加载器,即Thread.currentThread().getContextClassLoader();,为何不是ServiceLoader.class.getClassLoader()这种方式?

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

我们手动打印二者得知,前者输出sun.misc.Launcher$AppClassLoader@14dad5dc,后者输出null。null对应引导类加载器BootstrapClassLoaderAppClassLoader为应用程序类加载器,也即我们自己程序的类加载器。如果用ServiceLoader.class.getClassLoader()的方式,这意味着让引导类加载器去加载三方插件mysql-connector-java包中类,这当然是不行的,因为引导类加载器只负责加载Java的核心类库,并要求包路径前缀为java、javax、sun等。

Thread.currentThread().getContextClassLoader();会去获取当前线程的上下文加载器,如果不手动设置(通常如此),则会去找父线程的上下文加载器,在我们的案例中没有父线程,仅有main主线程,由我们的应用程序创建,故其类加载器为应用程序类加载器,可以加载三方插件中的类。

类似这种一个类自身的加载器无法加载它想加载的其他类时,往往都会利用线程上下文加载器去打破这种限制(即传统的双亲委派模型),这种行为在Spring、Dubbo等知名开源项目中更是不遑多见(它们都对SPI机制做了某种升级改造),但最终的目的都是为了减少开发者工作量,简单的SPI机制可以让我们少写一行代码,这些复杂的开源框架必会将更多的优秀代码埋藏深处,等待有心人发现。

相关文章:

以mysql驱动为案例,从源码角度深入分析Java的SPI机制

本文将以mysql驱动为案例&#xff0c;深入跟踪源码分析Java的SPI&#xff08;Service Provider Interface&#xff09;机制。 环境 java 8&#xff0c;mysql8.0&#xff0c;mysql-connector-java 8.0.20 代码 public class MysqlConnectorTest {public static void main(St…...

市盈率(P/E Ratio):理解股票价格与盈利的关系(中英双语)

市盈率&#xff08;P/E Ratio&#xff09;&#xff1a;理解股票价格与盈利的关系 今天在阅读《漫步华尔街》&#xff08;原书第13版&#xff09;的过程中&#xff0c;看到了“股票价格是每股盈利的 6 倍”的类似表述&#xff0c;于是产生了本文。 在投资股票时&#xff0c;投资…...

尚硅谷爬虫note008

一、handler处理器 定制更高级的请求头 # _*_ coding : utf-8 _*_ # Time : 2025/2/17 08:55 # Author : 20250206-里奥 # File : demo01_urllib_handler处理器的基本使用 # Project : PythonPro17-21# 导入 import urllib.request from cgitb import handler# 需求&#xff…...

AWS上基于高德地图API验证Amazon Redshift里国内地址数据正确性的设计方案

该方案通过无服务架构实现高可扩展性&#xff0c;结合分页查询和批量更新确保高效处理海量数据&#xff0c;同时通过密钥托管和错误重试机制保障安全性及可靠性。 一、技术栈 组件技术选型说明计算层AWS Lambda无服务器执行&#xff0c;适合事件驱动、按需处理&#xff0c;成…...

matlab汽车动力学半车垂向振动模型

1、内容简介 matlab141-半车垂向振动模型 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...

【新品解读】AI 应用场景全覆盖!解码超高端 VU+ FPGA 开发平台 AXVU13F

「AXVU13F」Virtex UltraScale XCVU13P Jetson Orin NX 继发布 AMD Virtex UltraScale FPGA PCIE3.0 开发平台 AXVU13P 后&#xff0c;ALINX 进一步研究尖端应用市场&#xff0c;面向 AI 场景进行优化设计&#xff0c;推出 AXVU13F。 AXVU13F 和 AXVU13P 采用相同的 AMD Vir…...

智能硬件定位技术发展趋势

在科技飞速进步的当下&#xff0c;智能硬件定位技术作为众多领域的关键支撑&#xff0c;正沿着多元且极具创新性的路径蓬勃发展&#xff0c;持续重塑我们的生活与工作方式。 一、精度提升的极致追求 当前&#xff0c;智能硬件定位精度虽已满足诸多日常应用&#xff0c;但未来…...

【Elasticsearch】`nested`和`flattened`字段在索引时有显著的区别

有同学问&#xff0c;nested查询效率不高为啥不直接扁平化查询呢&#xff1f;就跟之前的普通结构查询一样&#xff0c;这就有些想当然了&#xff0c;因为扁平化的结构在存储时&#xff0c;其实跟我们想的不一样&#xff0c;接下来给出扁平化在索引时的存储结构(尤其是当嵌套对象…...

【Linux探索学习】第二十七弹——信号(上):Linux 信号基础详解

Linux学习笔记&#xff1a; https://blog.csdn.net/2301_80220607/category_12805278.html?spm1001.2014.3001.5482 前言&#xff1a; 前面我们已经将进程通信部分讲完了&#xff0c;现在我们来讲一个进程部分也非常重要的知识点——信号&#xff0c;信号也是进程间通信的一…...

redis解决高并发看门狗策略

当一个业务执行时间超过自己设定的锁释放时间&#xff0c;那么会导致有其他线程进入&#xff0c;从而抢到同一个票,所有需要使用看门狗策略&#xff0c;其实就是开一个守护线程&#xff0c;让守护线程去监控key&#xff0c;如果到时间了还未结束&#xff0c;就会将这个key重新s…...

Ollama+DeepSeek+Open-WebUi

环境准备 Docker Ollama Open-WebUi Ollama 下载地址&#xff1a;Ollama docker安装ollama docker run -d \ -v /data/ollama/data:/root/.ollama \ -p 11434:11434 \ --name ollama ollama/ollama 下载模型 Ollama模型仓库 # 示例&#xff1a;安装deepseek-r1:7b doc…...

MySQL-事务隔离级别

事务有四大特性&#xff08;ACID&#xff09;&#xff1a;原子性&#xff0c;一致性&#xff0c;隔离性和持久性。隔离性一般在事务并发的时候需要保证事务的隔离性&#xff0c;事务并发会出现很多问题&#xff0c;包括脏写&#xff0c;脏读&#xff0c;不可重复读&#xff0c;…...

对于简单的HTML、CSS、JavaScript前端,我们可以通过几种方式连接后端

1. 使用Fetch API发送HTTP请求&#xff08;最简单的方式&#xff09;&#xff1a; //home.html // 示例&#xff1a;提交表单数据到后端 const submitForm async (formData) > {try {const response await fetch(http://your-backend-url/api/submit, {method: POST,head…...

从入门到精通:Postman 实用指南

Postman 是一款超棒的 API 开发工具&#xff0c;能用来测试、调试和管理 API&#xff0c;大大提升开发效率。下面就给大家详细讲讲它的安装、使用方法&#xff0c;再分享些实用技巧。 一、安装 Postman 你能在 Postman 官网&#xff08;https://www.postman.com &#xff09;下…...

error: conflicting types for ‘SSL_SESSION_get_master_key’

$ make make all-am make[1]: Entering directory ‘/home/linuxuser/tor’ CC src/lib/tls/libtor_tls_a-tortls_openssl.o In file included from src/lib/tls/tortls_openssl.c:61: ./src/lib/tls/tortls_internal.h:55:8: error: conflicting types for ‘SSL_SESSION_get_…...

sql sqlserver的特殊函数COALESCE和PIVOT的用法分析

一、COALESCE是一个返回参数中第一个非NULL值的函数&#xff0c; 列如&#xff1a;COALESCE&#xff08;a,b,c,d,e&#xff09;;可以按照顺序取abcde&#xff0c;中的第一个非空数据&#xff0c;abcde可以是表达式 用case when 加ISNULL也可以实现&#xff0c;但是写法复杂了…...

智能猫眼实现流程图

物理端开发流程图 客户端端开发流程图 用户功能开发流程图 管理员开发流程图...

c/c++蓝桥杯经典编程题100道(19)汉诺塔问题

汉诺塔问题 ->返回c/c蓝桥杯经典编程题100道-目录 目录 汉诺塔问题 一、题型解释 二、例题问题描述 三、C语言实现 解法1&#xff1a;递归法&#xff08;难度★&#xff09; 解法2&#xff1a;迭代法&#xff08;难度★★★&#xff09; 四、C实现 解法1&#xff1…...

蓝桥杯单片机大模板(西风)

#include <REGX52.H> #include "Key.h" #include "Seg.h" //变量声明区 unsigned char Key_Val,Key_Down,Key_Old;//按键扫描专用变量 unsigned char Key_Slow_Down;//按键减速专用变量 10ms unsigned int Seg_Slow_Down;//按键扫描专用变量 500ms …...

【Java基础】静态多态和动态多态

多态&#xff08;Polymorphism&#xff09; 多态是面向对象编程&#xff08;OOP&#xff09;中的核心概念之一&#xff0c;它指的是 同一接口&#xff0c;多个实现方式。在 Java 中&#xff0c;多态主要有 两种形式&#xff1a; 静态多态&#xff08;Static Polymorphism&…...

Android 10.0 移除wifi功能及相关菜单

介绍 客户的机器没有wifi功能&#xff0c;所以需要删除wifi相关的菜单&#xff0c;主要有设置-网络和互联网-WLAN,长按桌面设置弹出的WALN快捷方式&#xff0c;长按桌面-微件-设置-WLAN。 修改 Android10 上直接将config_show_wifi_settings改为false,这样wifi菜单的入口就隐…...

【LeetCode Hot100 子串】和为 k 的子数组、滑动窗口最大值、最小覆盖子串

子串 1. 和为 k 的子数组题目描述解题思路主要思路步骤 时间复杂度与空间复杂度代码实现 2. 滑动窗口最大值题目描述解题思路双端队列的原理&#xff1a;优化步骤&#xff1a; Java实现 3. 最小覆盖子串题目描述解题思路滑动窗口的基本思路&#xff1a;具体步骤&#xff1a;算法…...

【kafka系列】Kafka如何实现高吞吐量?

目录 1. 生产者端优化 核心机制&#xff1a; 关键参数&#xff1a; 2. Broker端优化 核心机制&#xff1a; 关键源码逻辑&#xff1a; 3. 消费者端优化 核心机制&#xff1a; 关键参数&#xff1a; 全链路优化流程 吞吐量瓶颈与调优 总结 Kafka的高吞吐能力源于其生…...

foobar2000设置DSP使用教程及软件推荐

foobar2000安卓中文版&#xff1a;一款高品质手机音频播放器 foobar2000安卓中文版是一款备受好评的高品质手机音频播放器。 几乎支持所有的音频格式&#xff0c;包括 MP3、MP4、AAC、CD 音频等。不论是经典老歌还是最新的流行音乐&#xff0c;foobar2000都能完美播放。除此之…...

【JAVA工程师从0开始学AI】,第四步:闭包与高阶函数——用Python的“魔法函数“重构Java思维

副标题&#xff1a;当严谨的Java遇上"七十二变"的Python函数式编程 历经变量战争、语法迷雾、函数对决&#xff0c;此刻我们将踏入Python最迷人的领域——函数式编程。当Java工程师还在用接口和匿名类实现回调时&#xff0c;Python的闭包已化身"智能机器人"…...

【R语言】回归分析与判别分析

一、线性回归分析 1、lm()函数 lm()函数是用于拟合线性模型&#xff08;Linear Models&#xff09;的主要函数。线性模型是一种统计方法&#xff0c;用于描述一个或多个自变量&#xff08;预测变量、解释变量&#xff09;与因变量&#xff08;响应变量&#xff09;之间的关系…...

AllData数据中台核心菜单十三:数据湖平台

&#x1f525;&#x1f525; AllData大数据产品是可定义数据中台&#xff0c;以数据平台为底座&#xff0c;以数据中台为桥梁&#xff0c;以机器学习平台为中层框架&#xff0c;以大模型应用为上游产品&#xff0c;提供全链路数字化解决方案。 ✨奥零数据科技官网&#xff1a;…...

Spring Cloud Gateway中断言路由和过滤器的使用

一&#xff0c;Gateway概念 Spring Cloud Gateway&#xff08;简称 Gateway&#xff09;是一个基于 Spring WebFlux 的 API 网关解决方案&#xff0c;旨在为微服务架构中的客户端提供路由、负载均衡、认证、限流、监控等功能。它作为微服务架构中的流量入口&#xff0c;通常位…...

Android 13 上通过修改 AOSP 拦截 SystemUI 音量调节事件

定位关键代码SystemUI 的音量调节逻辑主要集中在以下类中: VolumeDialogController.java:负责与 AudioService 交互。 VolumeDialogImpl.java:处理 UI 交互事件(如按钮点击)。 PhoneWindowManager.java:处理物理按键事件(如音量键)。 拦截音量调节事件 以 VolumeDialog…...

AcWing 798. 差分矩阵

题目来源&#xff1a; 找不到页面 - AcWing 题目内容&#xff1a; 输入一个 n 行 m 列的整数矩阵&#xff0c;再输入 q 个操作&#xff0c;每个操作包含五个整数 x1,y1,x2,y2,c&#xff0c;其中 (x1,y1) 和 (x2,y2)表示一个子矩阵的左上角坐标和右下角坐标。 每个操作都要将…...