使用SPI机制加载MySQL驱动源码分析

Posted wen-pan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用SPI机制加载MySQL驱动源码分析相关的知识,希望对你有一定的参考价值。

一、SPI介绍

SPI具体介绍,可以参考以前的文章:https://blog.csdn.net/Hellowenpan/article/details/101112365?spm=1001.2014.3001.5501


二、为什么要使用SPI机制加载driver

  • 主要原因是为了实现解耦和可插拔
    • JDK提供数据库驱动的规范(即Driver接口)但不提供任何的实现,位于java.sql包下
    • 各个不同的数据库厂商按照JDK提供的规范去进行不同的实现(比如mysql驱动实现,Oracle数据库驱动实现)
    • JVM启动的时候由启动类加载器加载java.sql包下的Driver接口类。
    • 但是各个厂商所实现的数据库驱动在JVM启动的时候,并不知道该从哪儿去加载具体的驱动实现类。那么JDK进制定了一套规范:
      • 你们各大数据库厂商要实现自己的数据库驱动,需要实现我JDK提供的Driver接口
      • 并且将你们的实现类打成jar包,并且在你们jar包中的META-INF/services目录下创建一个以java.sql.Driver命名的文件,文件中写上你们的驱动具体实现类名即可。
      • 然后将你们的jar包放到程序的classpath下,我JVM启动的时候自有我的办法去把你们的驱动实现加载进来(这里所谓的办法就是 利用SPI机制加载classpath下的驱动实现)。

三、使用SPI机制加载MySQL驱动源码分析

说明:关于什么是SPI以及SPI如何使用,这里不过多介绍,这里主要是分析JDK中是如何使用SPI机制来实现可插拔的数据库驱动加载的。关于SPI的一些介绍,可参考:https://blog.csdn.net/Hellowenpan/article/details/101112365?spm=1001.2014.3001.5501

①、大体流程分析

  • JVM启动的时候,首先由启动类加载器去加载JAVA_HOME/jre/lib目录下的类
  • 当加载到DriverManager类的时候会触发静态代码块的执行,在静态代码块中会执行loadInitialDrivers();代码
  • loadInitialDrivers();方法中会从启动类加载器切换到应用类加载器,然后通过应用类加载器去加载classpath下的mysql驱动到内存中。
    • 应用类加载器首先会读取并解析classpath下的MySQL驱动jar包中的META-INF/services/目录下的配置文件,然后解析配置文件中的驱动类名
    • 根据上一步解析的驱动类名,使用Class.forName()将类加载进来,然后使用反射来为该Class创建一个实例对象

②、源码分析

1、DriverManager被启动类加载器加载
public class DriverManager {
  static {
      // 启动类加载器加载DriverManager类,触发静态方法执行
      loadInitialDrivers();
      println("JDBC DriverManager initialized");
  }
}
2、loadInitialDrivers触发加载
  • 下面方法会创建ServiceLoader对象,使用ServiceLoader对象来切换到上下文类加载器
  • 重点需要关注ServiceLoader类和他的迭代器,核心逻辑就在这两个地方
private static void loadInitialDrivers() {
  	
    String drivers;
// ==========================================核心实现如下==========================================
  	// 加载java.sql.Driver驱动的实现
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

          	// 1、创建一个 ServiceLoader对象,【这里就将上下文类加载器设置到ServiceLoader对象的变量上了】,可自行查看
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 2、创建一个迭代器对象(这里只是创建,暂时不会涉及到迭代器的任何方法调用)
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            try{
              	// 3、这里调用driversIterator.hasNext()的时候,触发将 META-INF/services 下的
              	// 配置文件中的数据读取进来,方便下面的next方法使用
                while(driversIterator.hasNext()) {
                  	// 4、【关键】:触发上面创建的迭代器对象的方法调用。这里才是具体加载的实现逻辑,非常不好找
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });
// ==========================================核心实现如上==========================================
}
3、META-INF/services下文件读取逻辑

我们知道如果要加载MySQL的驱动,那么需要读取MySQL的jar包下的META-INF/services配置文件中的数据。那么整个流程中是在哪儿去读取的META-INF/services配置文件呢??? 其实非常隐蔽,在迭代器的hasNext()方法中触发的配置文件读取操作。源码如下

// 文件读取前缀
private static final String PREFIX = "META-INF/services/";

private boolean hasNextService() {
  
    if (nextName != null) {
        return true;
    }
  
    if (configs == null) {
        try {
          	// 1、拼凑要读取的文件的全名
            String fullName = PREFIX + service.getName();
          
            // 2、根据 fullName 去到META-INF/services/目录下寻找配置文件
          	// 如果类加载器为空,则使用系统类加载器,如果不为空则使用指定的类加载器
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
              	// 由应用类加载器从classpath下的META-INF/services/目录下读取
                configs = loader.getResources(fullName);
          
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
      	// 3、使用parse方法解析配置文件中的每一行数据
        pending = parse(service, configs.nextElement());
    }
    nextName = pending.next();
    return true;
}
4、使用反射创建对象逻辑

从上面一步分析中我们已经知道,META-INF/services配置文件是在hasNext()方法被调用时,懒惰的被读取的。此时被读取进来的仅仅是一个驱动实现类类名,并不是实际的对象。那么是在什么地方将MySQL驱动类实例化的呢???其实就是在迭代器的next()方法中,源码如下

public S next() {
  	// 可以看到next方法内部又调用了nextService方法,如下
    if (acc == null) {
        return nextService();
    } else {
        PrivilegedAction<S> action = new PrivilegedAction<S>() {
            public S run() { return nextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
  
  	// 通过classloader + 类名称 来加载驱动类
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,"Provider " + cn + " not found");
    }
 
  	// 使用反射newInstance实例化一个对象
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,"Provider " + cn + " could not be instantiated",x);
    }
   
}
5、其他

创建迭代器代码逻辑如下:

// 注意,这里lookupIterator是懒加载迭代器 LazyIterator
private LazyIterator lookupIterator;

public Iterator<S> iterator() {
  	// 创建一个迭代器(这里直接创建的是匿名对象)。后面调用迭代器遍历的时候就会吊起这里的方法
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();


        public boolean hasNext() {
          	// 先查缓存,如果缓存中存在,则直接返回true。很显然我们这里缓存中不存在
            if (knownProviders.hasNext())
                return true;
          	// 缓存中不存在,则调用lookupIterator.hasNext()。这里的lookupIterator的类型是【 LazyIterator 】
            return lookupIterator.hasNext();
        }

      
        public S next() {
          	// 先查缓存,如果缓存中存在,则直接返回缓存中的值
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
          	// 缓存中不存在,则调用lookupIterator.next()去查找
            return lookupIterator.next();
        }

    };
}

👉 注意:DriverManager本身是被启动类加载器加载的,只是在加载DriverManager类的时候会触发调用static方法,在static方法中使用的是SPI机制来切换到上下文类加载器,然后使用上下文类加载器来加载classpath下的MySQL驱动。

以上是关于使用SPI机制加载MySQL驱动源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Java SPI机制实战详解及源码分析

SPI机制到底是什么?中间件又是如何使用的?

源码分析---SOFARPC可扩展的机制SPI

dubbo源码分析01:SPI机制

Java SPI 机制在 Flink 中的应用(源码分析)

JDK的SPI原理及源码分析