采用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

相关文章

如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解

《如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别详解》:本文主要介绍如何通过海康威视设备网络SDK进行Java二次开发摄像头车牌识别的相关资料,描述了如何使用海康威视设备网络SD... 目录前言开发流程问题和解决方案dll库加载不到的问题老旧版本sdk不兼容的问题关键实现流程总结前言作为

SpringBoot中使用 ThreadLocal 进行多线程上下文管理及注意事项小结

《SpringBoot中使用ThreadLocal进行多线程上下文管理及注意事项小结》本文详细介绍了ThreadLocal的原理、使用场景和示例代码,并在SpringBoot中使用ThreadLo... 目录前言技术积累1.什么是 ThreadLocal2. ThreadLocal 的原理2.1 线程隔离2

springboot将lib和jar分离的操作方法

《springboot将lib和jar分离的操作方法》本文介绍了如何通过优化pom.xml配置来减小SpringBoot项目的jar包大小,主要通过使用spring-boot-maven-plugin... 遇到一个问题,就是每次maven package或者maven install后target中的ja

Java中八大包装类举例详解(通俗易懂)

《Java中八大包装类举例详解(通俗易懂)》:本文主要介绍Java中的包装类,包括它们的作用、特点、用途以及如何进行装箱和拆箱,包装类还提供了许多实用方法,如转换、获取基本类型值、比较和类型检测,... 目录一、包装类(Wrapper Class)1、简要介绍2、包装类特点3、包装类用途二、装箱和拆箱1、装

如何利用Java获取当天的开始和结束时间

《如何利用Java获取当天的开始和结束时间》:本文主要介绍如何使用Java8的LocalDate和LocalDateTime类获取指定日期的开始和结束时间,展示了如何通过这些类进行日期和时间的处... 目录前言1. Java日期时间API概述2. 获取当天的开始和结束时间代码解析运行结果3. 总结前言在J

Java深度学习库DJL实现Python的NumPy方式

《Java深度学习库DJL实现Python的NumPy方式》本文介绍了DJL库的背景和基本功能,包括NDArray的创建、数学运算、数据获取和设置等,同时,还展示了如何使用NDArray进行数据预处理... 目录1 NDArray 的背景介绍1.1 架构2 JavaDJL使用2.1 安装DJL2.2 基本操

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

Java多线程父线程向子线程传值问题及解决

《Java多线程父线程向子线程传值问题及解决》文章总结了5种解决父子之间数据传递困扰的解决方案,包括ThreadLocal+TaskDecorator、UserUtils、CustomTaskDeco... 目录1 背景2 ThreadLocal+TaskDecorator3 RequestContextH

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

java父子线程之间实现共享传递数据

《java父子线程之间实现共享传递数据》本文介绍了Java中父子线程间共享传递数据的几种方法,包括ThreadLocal变量、并发集合和内存队列或消息队列,并提醒注意并发安全问题... 目录通过 ThreadLocal 变量共享数据通过并发集合共享数据通过内存队列或消息队列共享数据注意并发安全问题总结在 J