JDBC与SPI机制

ServiceLoader方式服务发现进行分析。它是Jdk为我们提供的根据接口动态加载实现类(SPI机制)的工具。

一、问题引出:

当我们在使用原生jdbc时通常写为以下格式:
在这里插入图片描述
我们在加载驱动类的时候的静态代码块会帮助我们进行注册,所以我们再DriverManager中能够取得连接。
但事实上,我们去掉第一行Class.forName后,我们依旧可以获得相应数据库的连接,那么数据库驱动类是什么时候加载的呢?虚拟机怎么会知道驱动类的路径呢?

二、准备知识

1、ServiceLoader类的使用。

再次我们只是简要的概括ServiceLoader的作用:
它是Jdk为我们提供的根据接口动态加载实现类(SPI机制)的工具。
详细内容请参考ServiceLoader类详解。

2、SPI机制的概念

SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中,在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类就是我们上文介绍的java.util.ServiceLoader。

3、上下文类加载器

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
  这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器来加载的;SPI的实现类是由系统类加载器来加载的。启动类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能委派给系统类加载器,因为它是系统类加载器的祖先类加载器。
  线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
  而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。
  线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。


个人理解:
ServiceLoader类位于rt.jar中,rt.jar是被Bootstrap ClassLoader(启动类加载器)加载,Bootstrap ClassLoader是Java类加载层次中最顶级的加载】
App ClassLoader称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件

三、jdbc与spi

文章的一开始我们就发现了数据库驱动接口实现类不需要我们显示的加载我们也能获取到数据库的连接,那他是如何做到的呢?我们想到了DriverManager类的静态代码块,因为DriverManager加载的过程中首先执行的就是静态代码块:
在这里插入图片描述

看到执行的函数的名字我们似乎看到了一线希望,继续跟进

   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;
        }
        //通过SPI机制的工具类ServiceLoader去加载驱动类
        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和驱动类路径加载驱动类
			//非常重要的一点,我们看到类加载器是获得的系统类加载器
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
}


通过阅读静态代码块,在静态代码块加载驱动类的方法有两种:
1、从系统属性中获取驱动类的全路径,然后通过Class.forname()的方式加载驱动类,在这里有一个问题需要注意为什么我们在代码中调用Class.forname()方法加载驱动类时没有传入类加载器参数,而在DirverManager的静态代码块中加载时需要传入类加载器呢?
Class.forname()默认会用本类的类加载器去加载给定的类,也就是说如果我们不传入类加载器参数,他会调用DirverManager类的类加载器去加载,我们知道DirverManager的类加载器是启动类加载器(BootstrapClassloader),而数据库驱动类时各个数据库厂商提供的,显然BootstrapClassloader是无法加载数据库驱动类的。而根据类加载器的双亲委派模型,第三方实现的类库应该由系统类加载器(AppClassLoader)去加载,然而BootstrapClassloader是类加载器的最高层,无法调用底层的AppClassLoader加载器,所以这是我们需要传入一个系统类加载器。
我们再代码中经常写的class.forname(driver),不需要传入类加载器参数的原因是我们的代码本来就由系统类加载器加载,不冲突。
2、第二种方式就是通过SPI机制去加载数据库驱动了。显然我们的实验就是通过这种方式进行的驱动类的加载,因为我们根本没有设置系统属性,不会是第一种方式。
通过ServiceLoader类的学习我们知道,如果是通过SPI机制加载的驱动类,那么在数据库实现类的jar包中应该有相应的services文件夹我们打开验证:
在这里插入图片描述

可见我们的推测是正确的。
如果还不放心我们可以将这个services文件夹删掉,通过测试发现文件删掉后报错。
那么问题又来了,上文我们提到DriverManager的类加载器是不能加载数据库驱动类的,那我们通过ServiceLoader工具加载数据库驱动类时用的类加载器是谁呢?
我们只能继续跟进代码来到ServiceLoader类的load()方法:
在这里插入图片描述

我们看到我们通过当前线程获得了上下文类加载器(即默认的系统类加载器),至于什么是上下文类加载器,请参照上文。

参考文章:
1、https://blog.csdn.net/qq_41894099/article/details/104558522

おすすめ

転載: blog.csdn.net/yangyangrenren/article/details/121324333