采用JDBC解释java SPI机制和线程上下文类加载器 —————— 开开开山怪

2024-02-07 07:48

本文主要是介绍采用JDBC解释java SPI机制和线程上下文类加载器 —————— 开开开山怪,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

采用JDBC解释JAVA SPI机制和线程上下文类加载器

SPI(Service Provider Interface)网上有关于SPI的解释,在这里我简单总结一下。

SPI机制可以做到将服务接口和真正的服务接口的实现类分开,可以增加程序的可扩展性,通过扫描规定的路径来进行实现类的获取,可以说是一种服务发现机制。

优点: 在面向对象的设计中,我们一般建议基于接口的编程,如果代码中涉及到具体的实现类,如果我们想要换一种实现方案就不得不更改代码,但是采用接口的方式,只要我们采用一种机制,可以使得我们能够获取接口的不同的实现类,那么我们的代码的灵活性就比较的高,这种机制就是SPI。

SPI机制的工作过程

当服务提供者提供了服务接口的实现类,当实现类打成jar包之后,在jar包的META-INF/services/ 建立一个以服务接口名称为文件名称的文件,并且文件的内容为该服务接口的实现类的名称,那么当应用程序需要这部分功能模块的时候,就能通过META-INF/services/下的这个配置文件找到对应的实现类的名称,可以进行加载并且实例化。这也是同时也是服务提供者需要遵守的规则。

下面我们举例子所说的服务提供者就是mysql-connector-java-5.1.46-bin.jar
服务接口就是java.sql.Driver.

栗子:
在我们程序中,需要链接数据库的时候,我们都会在工程中导入一个jar包,数据库驱动jar包在我们的用户类路径中。当然关于实现java.sql.Driver接口的就是在jar包中的。
我的jar包就是这个mysql-connector-java-5.1.46-bin.jar

在这里插入图片描述

然后我们在我们自己的程序的代码中会写像下面这样的的代码进行数据库的链接,在第一句代码执行的时候就开始加载com.mysql.jdbc.Driver这个类了,并且采用的是加载当前类的系统类加载进行加载,因为com.mysql.jdbc.Driver这个类是在我们用户类路径中mysql-connector-java-5.1.46-bin.jar中所实现java.sql.Driver接口的实现类,系统类加载器可以直接加载这个类。

图一:
数据库链接述
之前在使用的过程中,不明白为什么这么写,只是内心默念一句,好了,这是神仙咒语,记住就好,管它什么意思。最近在看线程上下文类加载器的时候牵扯到了这部分,中间也牵扯到了SPI机制,所以就拿这个举例子进行线程上下文加载器和SPI机制的说明。

其实上面代码中Class.forName(“com.mysql.jdbc.Driver”)这句代码不用写。

图二:在这里插入图片描述像图二这样的代码也会正常运行滴,主要是因为SPI和线程上下文加载器应用的原因。

下面详细解释为什么图二代码可以正常运行,并且一同解释SPI机制的工作原理,和线程上下文类加载器的作用。

这部分代码是在我们用户自己的程序中写的,那么当我们用户代码需要运行的时候,首先会加载我们的主类,当然加载我们主类的类加载器是系统类加载器,当主类的代码运行到DriverManager.getConnection(url, “KSG”, “99999”);的时候,我们知道调用了一个类的静态方法,那么会对这个类进行初始化,同时也要加载类DriverManager,那么在初始化的时候当然是执行该类的类构造器()方法,那么就会执行该类中的静态块中的方法。

下面是DriverManager类的静态块的方法:

DriverManager在rt.jar中的java.sql下:加载DriverManager类的类加载器为启动类加载器
DriverManager类是驱动程序管理类

public class DriverManager {  private DriverManager(){}static {loadInitialDrivers();//加载数据库驱动程序,初始化时执行这个方法println("JDBC DriverManager initialized");}}

图三:

在这里插入图片描述
下面看loadInitialDrivers方法,

private static void loadInitialDrivers() {String drivers;try {//这种方式是通过系统的属性得到驱动程序的实现类的名称,上面的图可以说明问题//将我们的驱动程序类通过属性的设置,在这个方法中便可以通过属性系统属性获取驱动类的名称;//如果我们在图三中不进行系统属性的设置,也是可以运行成功的,这里只是为了解释System.getProperty("jdbc.drivers")这句代码,所以给出的图三。drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {//通过系统属性获取驱动程序的类的名称,//如果获取不到,没有关系,原因是图三的写法其实和图一的写法效果一样//但是没有图三和图一的第一句,程序依然可以成功执行,原因就在下面的 AccessController.doPrivileged这段代码中。return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}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;}});println("DriverManager.initialize: jdbc.drivers = " + drivers);//下面就是对通过系统属性获得的驱动程序的类的名称的处理//通过类名称采用系统类加载器进行类的加载if (drivers == null || drivers.equals("")) {return;//表示没有设置系统属性}String[] driversList = drivers.split(":");println("number of Drivers:" + driversList.length);for (String aDriver : driversList) {try {println("DriverManager.Initialize: loading " + aDriver);//进行驱动程序类的加载,采用系统加载器进行加载Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}}

上述代码中中间一部分没有写注释,其实中间部分是我所要说的重点。

loadInitialDrivers方法是在初始化DriverManager 类的时候执行的,DriverManager类由启动类加载器加载,当然在loadInitialDrivers方法中如果用到某个类还没有加载,当然是采用启动类加载器进行加载。

ServiceLoader.load(Driver.class)这句代码调用了ServiceLoader类中的静态方法,所以需要加载ServiceLoader类,由启动类加载器进行加载该类,然后执行load方法。

第一句:
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);

Driver.class 位于rt.jar包下 java.sql.Driver.class是一个接口

ServiceLoader这个类其实是在rt.jar java.util下的类,这个类的作用就是用来查找服务,可以说是jdk提供的一个实现服务查找的一个工具类。同样也是SPI机制中的一部分。

在DriverManager驱动程序管理类中用到这个ServiceLoader类当然是需要查找相关驱动程序接口的服务。

那很明显这里想要查找的服务是Driver服务,也就是数据库连接的驱动程序的实现类,因为Driver是java sql下的一个接口,在博文最开头已经说过了,SPI机制就是将接口与实现类进行分离,但同时也是一种服务发现机制,这里已经开始有些SPI的苗头了。

public static <S> ServiceLoader<S> load(Class<S> service) {
//这个ServiceLoader类中的load方法,采用java.lang.Thread类中getContextClassLoader方法获取类加载器,获取的类加载器默认为系统类加载器ClassLoader cl = Thread.currentThread().getContextClassLoader();     return ServiceLoader.load(service, cl);}
}

第二句和第三句:

Iterator driversIterator = loadedDrivers.iterator();
while ( driversIterator.hasNext () ) {
driversIterator.next();
}
上面说到需要查找的服务为Driver服务,那么这里就说到真正的SPI机制,
因为ServiceLoader这个类是用于查找实现类的,所有此类中有一个类的静态变量为private static final String PREFIX = “META-INF/services/”;

public final class ServiceLoader<S> implements Iterable<S>{private static final String PREFIX = "META-INF/services/";private final ClassLoader loader;private final ClassLoader loader;private final AccessControlContext acc;private LinkedHashMap<String,S> providers = new LinkedHashMap<>();private LazyIterator lookupIterator;
}

hasNext()方法中会将"META-INF/services/" + 服务接口的全称作为最终的查找文件的路径 ,因为服务接口是Driver,那么此时的文件查找路径就是"META-INF/services/java.sql.Driver.

hasNext()方法调用了ServiceLoader中的hasNextService()方法:
截取了一部分代码进行说明

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);}}}

因为传入的服务是Driver.class 那么此时的service.getName()就是java.sql.Driver

fullName = PREFIX + service.getName()
= META-INF/services/ java.sql.Driver

configs = loader.getResources(fullName);
这里的loader就是通过ServiceLoader.load(Driver.class)中的load方法所获得的的线程上下文类加载器(即默认的系统类加载器)

loader.getResources(fullName) 就是获取这个fullName文件中的实现类的名称,也就是下面的两个名称
(com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver)
名称为最后一张图的右边内容。

这就对照了我们开头所说的,一个服务提供者需要在自己的jar包中
"META-INF/services/"路径下创建一个以服务接口为名称的文件,并将自己的实现类的名称写入。并且我们的服务提供者就是mysql-connector-java-5.1.46-bin.jar

因为mysql-connector-java-5.1.46-bin.jar实现java.sql.Driver的接口,所以同样也遵守相应的规则,从最后一张图就可以看出来。

driversIterator.next()调用了ServiceLoader中的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");}}

我们很明显看到,在方法中有一句代码是
c = Class.forName(cn, false, loader);这句话事实上就代替了图一的第一句话
Class.forName(name),所以开头说过图一的第一行代码完全不用写也可以运行。

像图一那样写的话,程序进行到Class.forname(name)时,我们就要对名为name的实现类进行初始化,同时也要加载名为name的类,那么图一的情况当然是采用加载当前主类的系统类加载器进行加载,合情合理。

但是在 nextService() 方法中为什么采用了三参,原因是如果我们在nextService() 方法中采用单参的Class.forname(name)会出现什么情况,当运行到Class.forname(name)的时候需要加载名为name的类,但是会采用加载当前的类的类加载器来加载名为name的类,当前类是ServiceLoader,而ServiceLoader是由启动类加载器进行加载的,我们又知道双亲委派的加载模式是自下而上的,启动类加载器没有父加载器,所以只能由自己进行加载,可是当前名为name的类是我们查找的在用户类路径下类库中的类。
启动类加载器无法加载,所以采用了三参模式。

c = Class.forName(cn, false, loader);
此时的cn就是上面通过SPI机制找到的位于用户类路径下的"META-INF/services/java.sql.Driver文件配置的类的名称
(com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver))也就是下图中的右边的内容。

这个loader就是上面ServiceLoader.load(Driver.class)的load代码中通过ClassLoader cl = Thread.currentThread().getContextClassLoader()获得的线程上下文类加载器(默认为系统类加载器)所以此时loader就是系统类加载器,那么此时可以加载名为com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver的实现类了。

因为这两个类是由服务提供者mysql-connector-java-5.1.46-bin.jar实现的类,并在mysql-connector-java-5.1.46-bin.jar中,所以这两个类是在用户类路径下的两个类,所有可由系统类加载器直接进行加载。但是这样便打破了了双亲委派加载模式,由于启动类记载器不能够进行加载,所以采用了线程上下文类加载器。也就是父加载器要求子类加载器去完成类加载。在这里插入图片描述mysql-connector-java-5.1.46-bin.jar这个jar包中的com.mysql.jdbc.Driver实现了我们之前说的rt.jar中的java.sql.Driver接口,那么mysql-connector-java-5.1.46-bin.jar相当于一个服务提供者,它在自己的jar包的"META-INF/services/路径下创建一个文件名为java.sql.Driver的文件,并且在文件中配置 了实现Driver接口的实现类的名称。

上面的例子同时也体现出了当基础类调用用户代码的时候,我们实际上采用的是一种打破双亲委派模型的的一种加载方式,因为我们知道双亲委派模型的加载方式是自下而上的,而这里采用的是线程上下文类加载器去加载所需要的SPI代码,也就是父加载器请求子加载器去完成类加载动作。

这篇关于采用JDBC解释java SPI机制和线程上下文类加载器 —————— 开开开山怪的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/687022

相关文章

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

Mac系统下卸载JAVA和JDK的步骤

《Mac系统下卸载JAVA和JDK的步骤》JDK是Java语言的软件开发工具包,它提供了开发和运行Java应用程序所需的工具、库和资源,:本文主要介绍Mac系统下卸载JAVA和JDK的相关资料,需... 目录1. 卸载系统自带的 Java 版本检查当前 Java 版本通过命令卸载系统 Java2. 卸载自定

springboot下载接口限速功能实现

《springboot下载接口限速功能实现》通过Redis统计并发数动态调整每个用户带宽,核心逻辑为每秒读取并发送限定数据量,防止单用户占用过多资源,确保整体下载均衡且高效,本文给大家介绍spring... 目录 一、整体目标 二、涉及的主要类/方法✅ 三、核心流程图解(简化) 四、关键代码详解1️⃣ 设置

Java Spring ApplicationEvent 代码示例解析

《JavaSpringApplicationEvent代码示例解析》本文解析了Spring事件机制,涵盖核心概念(发布-订阅/观察者模式)、代码实现(事件定义、发布、监听)及高级应用(异步处理、... 目录一、Spring 事件机制核心概念1. 事件驱动架构模型2. 核心组件二、代码示例解析1. 事件定义

SpringMVC高效获取JavaBean对象指南

《SpringMVC高效获取JavaBean对象指南》SpringMVC通过数据绑定自动将请求参数映射到JavaBean,支持表单、URL及JSON数据,需用@ModelAttribute、@Requ... 目录Spring MVC 获取 JavaBean 对象指南核心机制:数据绑定实现步骤1. 定义 Ja

javax.net.ssl.SSLHandshakeException:异常原因及解决方案

《javax.net.ssl.SSLHandshakeException:异常原因及解决方案》javax.net.ssl.SSLHandshakeException是一个SSL握手异常,通常在建立SS... 目录报错原因在程序中绕过服务器的安全验证注意点最后多说一句报错原因一般出现这种问题是因为目标服务器

Java实现删除文件中的指定内容

《Java实现删除文件中的指定内容》在日常开发中,经常需要对文本文件进行批量处理,其中,删除文件中指定内容是最常见的需求之一,下面我们就来看看如何使用java实现删除文件中的指定内容吧... 目录1. 项目背景详细介绍2. 项目需求详细介绍2.1 功能需求2.2 非功能需求3. 相关技术详细介绍3.1 Ja

springboot项目中整合高德地图的实践

《springboot项目中整合高德地图的实践》:本文主要介绍springboot项目中整合高德地图的实践,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一:高德开放平台的使用二:创建数据库(我是用的是mysql)三:Springboot所需的依赖(根据你的需求再

spring中的ImportSelector接口示例详解

《spring中的ImportSelector接口示例详解》Spring的ImportSelector接口用于动态选择配置类,实现条件化和模块化配置,关键方法selectImports根据注解信息返回... 目录一、核心作用二、关键方法三、扩展功能四、使用示例五、工作原理六、应用场景七、自定义实现Impor

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项