面试官:JDBC 是如何打破双亲委派模型的?

708 阅读4分钟

大家好,我是大明哥,一个专注 「死磕 Java」 的硬核程序员。

回答

在 JDBC 4.0之后使用spi机制才会破坏双亲委派机制。

java.sql.DriverManager 在初始化时会调用 ServiceLoader 的 load() 去加载实现了java.sql.Driver接口的实现类。ServiceLoader 会搜索当前类路径上所有JAR文件内的META-INF/services/java.sql.Driver文件,从这些文件中读取实现类的全限定名。假如我们加载到了 MySQL 的驱动程序,获取的实现类为 com.mysql.jdbc.Driver,但是 DriverManager 位于 rt.jar 包,类加载器是启动类加载器,而 com.mysql.jdbc.Driver 位于 MySQL 驱动程序的 jar 文件中,启动类加载器是无法加载该类的,那怎么办呢?

添加一个线程上下文类加载器(Thread Context ClassLoader),在启动类加载器中获取应用程序类加载器。有了线程上下文类加载器,应用程序就可以把原本需要由启动类加载器进行加载的类,改为由应用程序类加载器来加载了,从而打破双亲委派模型。

详情

原生 JDBC 中的 Driver 驱动仅仅只是一个接口,具体的实现有不同的数据库厂商来提供,例如 MySQL 数据库提供的 com.mysql.cj.jdbc.Driver

DriverManager 是一个管理 JDBC 驱动的类,它主要提供两个功能:

  1. 驱动注册:当 JDBC 驱动类被加载到JVM时,它应该调用 DriverManager.registerDriver() 将自身注册到 DriverManager 中。
  2. 管理连接:调用DriverManager.getConnection(url, user, password) 可以获取与数据库的连接。这个方法会检查已注册的驱动,找到能够处理提供的URL的合适驱动,并通过该驱动建立连接。

未破坏双亲委派机制的情况

下面这段代码是我们连接数据库最基本的用法:

 String url = "jdbc:mysql://localhost:3306/skjava";
 String username = "root";
 String password = "123456";
 // 注册驱动
 Class.forName("com.mysql.cj.jdbc.Driver");
 // 获取连接
 Connection connection = DriverManager.getConnection(url, username, password);

我们显示使用 Class.forName("com.mysql.cj.jdbc.Driver"); 的方式来进行驱动的加载,加载器是使用的调用当前方法所用的类加载器,这个过程是遵循双亲委派模型的。

破坏双亲委派机制的情况

注:下面例子以 MySQL 为例。

从 JDBC 4.0 开始,引入了基于服务提供者接口(SPI)的方式来注册 Driver。通过这种方式我们就不需要显示调用 Class.forName() 来加载驱动程序了。

具体做法是,在 MySQL jar 包中的META-INF/services/java.sql.Driver 文件中会指明实现 Driver 接口的实现类是哪一个,例如:

在使用的时候我们就不需要显示调用 Class.forName("com.mysql.cj.jdbc.Driver") 来指明 MySQL 的驱动类了,由 DriverManager 自动发现。

DriverManager 类在初始化时会调用 loadInitialDrivers() 来初始化驱动程序:

public class DriverManager {

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

loadInitialDrivers()

    private static void loadInitialDrivers() {
        //...

        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;
            }
        });

        //...
    }

ServiceLoader 会扫描当前类路径上所有JAR文件内的META-INF/services/java.sql.Driver文件,从这些文件中读取实现类的全限定名。然后迭代(ServiceLoader 实现了 Iterable 接口,其本身就是一个迭代器)。这里调用 driversIterator.next() 是 ServiceLoader 中的

            public S next() {
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                return lookupIterator.next();
            }

我们一路跟进会到这里:

        private S nextService() {
            //...
            try {
                // cn就是数据库厂商提供的类名类名
                // loader 就是类加载器
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            //...
            throw new Error();          // This cannot happen
        }

loader 是加载 cn 的类加载器。我们知道DriverManager 位于 rt.jar 包,它类加载器是启动类加载器,而 com.mysql.jdbc.Driver 位于 MySQL 驱动程序的 jar 文件中,是无法通过启动类加载器来加载的。所以它由 loader 来加载,那这个 loader 是在什么时候指定的?构造 ServiceLoader 对象的时候:

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        reload();
    }

那什么时候构造 ServiceLoader 的呢?DriverManager#loadInitialDrivers() 中:

    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);
    }

从这里我们可以看出加载 com.mysql.jdbc.Driver 的加载器是通过 Thread.currentThread().getContextClassLoader()来获取的,这里获取的是应用程序类加载器。

所以,由于DriverManager 无法加载 MySQL jar 中的驱动程序,我们通过线程上下文类加载器拿到应用程序类加载器,同时也在 MySQL 的 jar 中获取到了具体的驱动实现类,这样我们就可以成功地在 rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了,从而打破双亲委派模型。

总结下:

  • META-INF/services/java.sql.Driver文件中获取 java.sql.Driver 接口的实现类名 "com.mysql.jdbc.Driver"。
  • 通过 Thread.currentThread().getContextClassLoader() 获取线程上下文类加载器,从而获取到应用程序类加载器。
  • 通过线程上下文类加载器去加载 com.mysql.jdbc.Driver,从而避开了双亲委派模型的弊端。

本文已收录到大明哥的「 Java 面试宝典」中。

💻 死磕 Java