使用springboot通过spi机制加载mysql驱动的过程
SPI是一种JDK提供的加载插件的灵活机制,分离了接口与实现,就拿常用的数据库驱动来说,我们只需要在spring系统中引入对应的数据库依赖包(比如mysql-connector-java以及针对oracle的ojdbc6驱动),然后在yml或者properties配置文件中对应的数据源配置就可自动使用对应的sql驱动,
比如mysql的配置:
spring:datasource:url: jdbc:mysql://localhost:3306/xxxxx?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghaiusername: devpassword: xxxxxxplatform: mysql
spi机制正如jdk的classloader一样,你不引用它,它是不会自动加载到jvm的,不是引入了下面的的两个sql驱动依赖就必然会加载oracle以及mysql的驱动:
com.oracle ojdbc612.1.0.1-atlassian-hosted mysql mysql-connector-javaruntime
【使用springboot通过spi机制加载mysql驱动的过程】正是由于jdk的这种spi机制,我们在spring项目中使用对应的驱动才这么简单,
我们只需做两件事:
1、在pom文件中引入对应的驱动依赖
2、在配置文件中配置对应的数据源即可
那么在spring项目中到底是谁触发了数据库驱动的spi加载机制呢?为了说明这个问题,咱们先说说jdk的spi的工作机制,jdk的spi通过ServiceLoader这个类来完成对应接口实现类的加载工作,就拿咱们要说的数据库驱动来说,
ServiceLoader会在spring项目的classpath中寻找那些满足下面条件的类:
1、这些jar包的META-INF/services有一个java.sql.Driver的文件
对应java.sql.Driver文件中为该数据库驱动对应的数据库驱动的实现类,比如mysql驱动对应的就是com.mysql.cj.jdbc.Driver,如下图所示:
文章图片
JDK这部分有关SPI具体的实现机制可以阅读下ServiceLoader的内部类LazyIterator,该类的hasNextService、nextService两个方法就是具体SPI机制工作底层机制。
好了,上面简要概述了下JDK的SPI工作机制,下面继续看spring框架如何使用spi机制来完成数据库驱动的自动管理的(加载、注销),接下来就按照事情发展的先后的先后顺序把mysql驱动加载的全过程屡一下,笔者使用的是springboot 2.x,数据源使用的数据源为Hikari,这是后来居上的一款数据源,凭借其优秀的性能以及监控机制成为了springboot 2.x之后首推的数据源,
用过springboot的小伙伴对springboot的自动装载机制,数据源的配置也是使用的自动装配机制,
具体类DataSourceAutoConfiguration
文章图片
注意上面标红部分,这里面引入的Hikari、Tomcat等(除了DataSourceJmxConfiguration之外)都是一些数据源配置,我们先看下
springboot推荐的Hikari数据源配置:
/**** 这是一个Configuration类,该类定义了创建HikariDataSource的Bean方法***/@Configuration @ConditionalOnClass(HikariDataSource.class) @ConditionalOnMissingBean(DataSource.class) @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "https://www.it610.com/article/com.zaxxer.hikari.HikariDataSource",matchIfMissing = true) static class Hikari { @Bean@ConfigurationProperties(prefix = "spring.datasource.hikari")public HikariDataSource dataSource(DataSourceProperties properties) {// 使用配置文件中的数据源配置来创建Hikari数据源HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) {dataSource.setPoolName(properties.getName()); }return dataSource; } }
由于在DataSourceAutoConfiguration类中首先引入的就是Hikari的配置,DataSource没有创建,满足ConditionalOnMissingBean以及其他一些条件,就会使用该配置类创建数据源,好了接下来看下createDataSource到底是怎么创建数据源的,
这个过程又是怎么跟SPI关联起来的
abstract class DataSourceConfiguration { @SuppressWarnings("unchecked") protected staticT createDataSource(DataSourceProperties properties, Class extends DataSource> type) {//使用DataSourceProperties数据源配置创建DataSourceBuilder对象(设计模式中的建造者模式)return (T) properties.initializeDataSourceBuilder().type(type).build(); } //下面看下DataSourceBuilder的build方法public T build() {//在该例子中,type返回的是com.zaxxer.hikari.HikariDataSource类Class extends DataSource> type = getType(); //实例化HikariDataSource类DataSource result = BeanUtils.instantiateClass(type); maybeGetDriverClassName(); //bind方法中会调用属性的设置,反射机制,在设置driverClassName属性时bind(result); return (T) result; } // HikariConfig的方法,HikariDataSource继承自HikariConfig类public void setDriverClassName(String driverClassName){checkIfSealed(); Class> driverClass = null; ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader(); try {if (threadContextClassLoader != null) {try {//加载driverClassName对应的类,即com.mysql.cj.jdbc.Driver类,该类为mysql对应的驱动类driverClass = threadContextClassLoader.loadClass(driverClassName); LOGGER.debug("Driver class {} found in Thread context class loader {}", driverClassName, threadContextClassLoader); }catch (ClassNotFoundException e) {LOGGER.debug("Driver class {} not found in Thread context class loader {}, trying classloader {}",driverClassName, threadContextClassLoader, this.getClass().getClassLoader()); }} if (driverClass == null) {driverClass = this.getClass().getClassLoader().loadClass(driverClassName); LOGGER.debug("Driver class {} found in the HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader()); }} catch (ClassNotFoundException e) {LOGGER.error("Failed to load driver class {} from HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader()); } if (driverClass == null) {throw new RuntimeException("Failed to load driver class " + driverClassName + " in either of HikariConfig class loader or Thread context classloader"); } try {// 创建com.mysql.cj.jdbc.Driver对象,接下来看下com.mysql.cj.jdbc.Driver创建对象过程中发生了什么driverClass.newInstance(); this.driverClassName = driverClassName; }catch (Exception e) {throw new RuntimeException("Failed to instantiate class " + driverClassName, e); }} // com.mysql.cj.jdbc.Driver类public class Driver extends NonRegisteringDriver implements java.sql.Driver {//// Register ourselves with the DriverManager//static {try {//调用DriverManager注册自身,DriverManager使用CopyOnWriteArrayList来存储已加载的数据库驱动,然后当创建连接时最终会调用DriverManager的getConnection方法,这才是真正面向数据库的,只不过spring的jdbc帮助我们屏蔽了这些细节java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) {throw new RuntimeException("Can't register driver!"); }}
上面已经来到了DriverManager类,那么DriverManager类里面是否有什么秘密呢,继续往下走,
看下DriverManager的重要方法:
static {//静态方法,jvm第一次加载该类时会调用该代码块loadInitialDrivers(); println("JDBC DriverManager initialized"); } //DriverManager类的loadInitialDrivers方法 private static void loadInitialDrivers() {String drivers; try {drivers = AccessController.doPrivileged(new PrivilegedAction() {public String run() {return System.getProperty("jdbc.drivers"); }}); } catch (Exception ex) {drivers = null; } AccessController.doPrivileged(new PrivilegedAction() {public Void run() {//这就是最终的谜底,最终通过ServiceLoader来加载SPI机制提供的驱动,本文用到了两个,一个是mysql的,一个是oracle的,注意该方法只会在jvm第一次加载DriverManager类时才会调用,所以会一次性加载所有的数据库驱动ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator driversIterator = loadedDrivers.iterator(); /* Load these drivers, so that they can be instantiated.* It may be the case that the driver class may not be there* i.e. there may be a packaged driver with the service class* as implementation of java.sql.Driver but the actual class* may be missing. In that case a java.util.ServiceConfigurationError* will be thrown at runtime by the VM trying to locate* and load the service.** Adding a try catch block to catch those runtime errors* if driver not available in classpath but it's* packaged as service and that service is there in classpath.*///下面的代码就是真正完成数据库驱动加载的地方,对应ServiceLoader类的LazyIterator类,所以看下该类的hasNext一级next方法即可,上面已经讲过,这里就不再赘述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); }}}
好了,上面已经把springboot如何使用jdk的spi机制来加载数据库驱动的,至于DriverManager的getConnection方法调用过程可以使用类似的方式分析下,在DriverManager的getConnection方法打个断点,当代码停在断点处时,通过Idea或者eclipse的堆栈信息就可以看出个大概了。
但愿本文能帮助一些人了解mysql驱动加载的整个过程,加深对SPI机制的理解。希望能给大家一个参考,也希望大家多多支持脚本之家。
推荐阅读
- 由浅入深理解AOP
- 【译】20个更有效地使用谷歌搜索的技巧
- Activiti(一)SpringBoot2集成Activiti6
- mybatisplus如何在xml的连表查询中使用queryWrapper
- MybatisPlus|MybatisPlus LambdaQueryWrapper使用int默认值的坑及解决
- MybatisPlus使用queryWrapper如何实现复杂查询
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- gitlab|gitlab 通过备份还原 admin/runner 500 Internal Server Error
- iOS中的Block