从JDBC看双亲委派模型与SPI

从JDBC看双亲委派模型与SPI

(一)概述

Java本身有一套资源管理服务JNDI(Java naming and directory interface,Java命名和目录接口),是放置在rt.jar中,由启动类加载器加载的,JDBC也在其中,我们就以我们最熟悉的JDBC为例,来看看为什么JDBC要打破双亲委派模型。

我们都知道,我们在使用JDBC时需要自己下载数据库厂商提供的数据库连接驱动jar包,这个jar包实际上就是Driver接口的实现类,下面是Driver接口:

public interface Driver {
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

JDK只能提供一个规范接口,而不能提供对应实现,这个要各数据库厂商去实现。各数据库厂商通过面向接口编程,实现这个Driver接口,从而屏蔽不同种类数据库的不同实现,使得不同的数据库可以使用相同的API来进行数据操作和查询。除此之外,还有一个负责管理Driver的类,就是DriverManager,大多数人应该都很熟悉,但不一定能理解背后的原理,我们先来看一下删减过的源码:

public class DriverManager {
    // 这里用来保存所有Driver的具体实现
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
        registerDriver(driver, null);
    }

    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);
    }
}

可以看到我们使用数据库驱动前必须先要在DriverManager中使用registerDriver()注册,然后我们才能正常使用。

(二)不破坏双亲委托模型

按照我们以前的习惯,我们会使用Class.forName()方法加载驱动,然后再通过DriverManager获取连接,就像这样:

// 1.加载数据访问驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.连接到数据库
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "123456");

很显然,Class.forName()方法触发了驱动类的加载,我们以MySQL的驱动类为例看看驱动类内部是如何注册Driver的。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

当驱动类被Class.forName()加载时,类内静态代码块将会被执行,然后MySQL实现的Driver对象就会实例化并且注册到DriverManager。我们通过DriverManager去获取connection的时候只要遍历当前所有Driver实现,然后选择一个建立连接就可以了。

(三)破坏双亲委托模型(SPI机制)

在讲这种加载模式之前,有必要先来了解一下SPI。

SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中,在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件,该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。

在JDBC4.0以后,开始支持使用SPI的方式来注册这个Driver,具体做法就是在MySQL的驱动jar包中的META-INF/services/java.sql.Driver文件中指明当前使用的Driver实现类,然后使用的时候就直接获取连接就可以了省去了上面的Class.forName()注册过程:

 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "123456");

我们来看利用了SPI的加载方式的流程是怎么样的:

  1. 从META-INF/services/java.sql.Driver文件中获取具体的实现类名为“com.mysql.jdbc.Driver”。
  2. 加载这个类,这里也是用class.forName(“com.mysql.jdbc.Driver”)来加载。

我们发现这种加载方式只是由手动加载变成了自动加载而已,似乎与之前的方式没有什么不同,但是这种加载会遇到一个很大的麻烦。按照类加载规则,Class.forName()加载用的是这个方法的调用者的Classloader,前面讲过这个调用者DriverManager是在rt.jar中的,它的ClassLoader是BootstrapClassLoader,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以BootstrapClassLoader肯定是无法加载这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。因此在这种情况下,我们需要破坏双亲委派模型的限制。

那么,这个问题如何解决呢?按照目前情况来分析,这个驱动只有AppClassLoader能加载,那么如果BootstrapClassLoader中有方法能够获取AppClassLoader,然后通过它去加载就可以了。这就是所谓的线程上下文加载器(ContextClassLoader)。ContextClassLoader可以通过Thread.setContextClassLoader()方法设置,如果不特殊设置会从父类继承,一般默认使用的是AppClassLoader。很明显,ContextClassLoader让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则。

我们可以先对SPI服务加载机制注册驱动的原理进行分析,重点就在DriverManager.getConnection()中。我们知道,调用类的静态方法会初始化该类,而执行其静态代码块是初始化类过程中必不可少的一环。下面是DriverManager的静态代码块:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

静态代码块里面调用了loadInitialDrivers()方法,下面列出这个方法的源码:

private static void loadInitialDrivers() {
    //省略代码
    //这里就是查找各个数据库厂商在自己的jar包中通过spi注册的驱动
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    try{
         while(driversIterator.hasNext()) {
         	driversIterator.next();
         }
    } catch(Throwable t) {
    	// Do nothing
    }
    //省略代码
}

从源码角度我们看出,SPI的加载流程和我们上面讲的过程完全相同,我们发现SPI加载位于ServiceLoader.load(Driver.class)方法,我们来看一下他的具体实现:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

可以看到代码的核心就是拿到ContextClassLoader,然后构造了一个ServiceLoader。ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

DriverManager中的loadInitialDrivers()方法中有一句driversIterator.next(),它的具体实现如下:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //此处的cn就是产商在META-INF/services/java.sql.Driver文件中注册的Driver具体实现类的名称
       //此处的loader就是之前构造ServiceLoader时传进去的ContextClassLoader
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service, "Provider " + cn + " not found");
    }
 	//省略部分代码
}

我们成功的做到了通过ContextClassLoader拿到了AppClassLoader,同时我们也查找到了数据库厂商在子级的jar包中注册的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了。

到这儿差不多把SPI机制解释清楚了,直白一点说就是JDK提供了一种帮第三方实现者加载服务(如数据库驱动、日志库)的便捷方式,只要遵循约定(把类名写在/META-INF里),那当启动时就会去扫描所有jar包里符合约定的类名的类,再调用forName加载。但这个调用者的ClassLoader是没法加载的,那就把它加载到当前执行线程的线程上下文类加载器里然后再去加载。

2020年9月16日

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 鲸 设计师:meimeiellie 返回首页