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

相关文章

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll