服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Java教程 - 使用springboot通过spi机制加载mysql驱动的过程

使用springboot通过spi机制加载mysql驱动的过程

2021-11-01 10:49PolarisHuster Java教程

这篇文章主要介绍了使用springboot通过spi机制加载mysql驱动的过程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

SPI是一种JDK提供的加载插件的灵活机制,分离了接口与实现,就拿常用的数据库驱动来说,我们只需要在spring系统中引入对应的数据库依赖包(比如mysql-connector-java以及针对oracle的ojdbc6驱动),然后在yml或者properties配置文件中对应的数据源配置就可自动使用对应的sql驱动,

比如mysql的配置:

?
1
2
3
4
5
6
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/xxxxx?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: dev
    password: xxxxxx
    platform: mysql

spi机制正如jdk的classloader一样,你不引用它,它是不会自动加载到jvm的,不是引入了下面的的两个sql驱动依赖就必然会加载oracle以及mysql的驱动:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--oracle驱动-->
<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
    <version>12.1.0.1-atlassian-hosted</version>
</dependency>
 
<!--mysql驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

正是由于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,如下图所示:

使用springboot通过spi机制加载mysql驱动的过程

JDK这部分有关SPI具体的实现机制可以阅读下ServiceLoader的内部类LazyIterator,该类的hasNextService、nextService两个方法就是具体SPI机制工作底层机制。

好了,上面简要概述了下JDK的SPI工作机制,下面继续看spring框架如何使用spi机制来完成数据库驱动的自动管理的(加载、注销),接下来就按照事情发展的先后的先后顺序把mysql驱动加载的全过程屡一下,笔者使用的是springboot 2.x,数据源使用的数据源为Hikari,这是后来居上的一款数据源,凭借其优秀的性能以及监控机制成为了springboot 2.x之后首推的数据源,

用过springboot的小伙伴对springboot的自动装载机制,数据源的配置也是使用的自动装配机制,

具体类DataSourceAutoConfiguration

使用springboot通过spi机制加载mysql驱动的过程

注意上面标红部分,这里面引入的Hikari、Tomcat等(除了DataSourceJmxConfiguration之外)都是一些数据源配置,我们先看下

springboot推荐的Hikari数据源配置:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 ** 这是一个Configuration类,该类定义了创建HikariDataSource的Bean方法
***/
 @Configuration
 @ConditionalOnClass(HikariDataSource.class)
 @ConditionalOnMissingBean(DataSource.class)
 @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "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关联起来的

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
abstract class DataSourceConfiguration {
 
    @SuppressWarnings("unchecked")
    protected static <T> T 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的重要方法:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
static {
    //静态方法,jvm第一次加载该类时会调用该代码块
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}
 
//DriverManager类的loadInitialDrivers方法
 
private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
 
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
         
            //这就是最终的谜底,最终通过ServiceLoader来加载SPI机制提供的驱动,本文用到了两个,一个是mysql的,一个是oracle的,注意该方法只会在jvm第一次加载DriverManager类时才会调用,所以会一次性加载所有的数据库驱动
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> 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机制的理解。希望能给大家一个参考,也希望大家多多支持服务器之家。

原文链接:https://jonhuster.blog.csdn.net/article/details/104394196

延伸 · 阅读

精彩推荐
  • Java教程Java8中Stream使用的一个注意事项

    Java8中Stream使用的一个注意事项

    最近在工作中发现了对于集合操作转换的神器,java8新特性 stream,但在使用中遇到了一个非常重要的注意点,所以这篇文章主要给大家介绍了关于Java8中S...

    阿杜7482021-02-04
  • Java教程xml与Java对象的转换详解

    xml与Java对象的转换详解

    这篇文章主要介绍了xml与Java对象的转换详解的相关资料,需要的朋友可以参考下...

    Java教程网2942020-09-17
  • Java教程Java使用SAX解析xml的示例

    Java使用SAX解析xml的示例

    这篇文章主要介绍了Java使用SAX解析xml的示例,帮助大家更好的理解和学习使用Java,感兴趣的朋友可以了解下...

    大行者10067412021-08-30
  • Java教程小米推送Java代码

    小米推送Java代码

    今天小编就为大家分享一篇关于小米推送Java代码,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧...

    富贵稳中求8032021-07-12
  • Java教程20个非常实用的Java程序代码片段

    20个非常实用的Java程序代码片段

    这篇文章主要为大家分享了20个非常实用的Java程序片段,对java开发项目有所帮助,感兴趣的小伙伴们可以参考一下 ...

    lijiao5352020-04-06
  • Java教程Java BufferWriter写文件写不进去或缺失数据的解决

    Java BufferWriter写文件写不进去或缺失数据的解决

    这篇文章主要介绍了Java BufferWriter写文件写不进去或缺失数据的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望...

    spcoder14552021-10-18
  • Java教程升级IDEA后Lombok不能使用的解决方法

    升级IDEA后Lombok不能使用的解决方法

    最近看到提示IDEA提示升级,寻思已经有好久没有升过级了。升级完毕重启之后,突然发现好多错误,本文就来介绍一下如何解决,感兴趣的可以了解一下...

    程序猿DD9332021-10-08
  • Java教程Java实现抢红包功能

    Java实现抢红包功能

    这篇文章主要为大家详细介绍了Java实现抢红包功能,采用多线程模拟多人同时抢红包,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙...

    littleschemer13532021-05-16