JDBC破坏双亲委派机制JDBC破坏双亲委派机制总结

JDBC破坏双亲委派机制

java有自己的一套 资源管理服务JNDI 等等,是由启动类加载器加载的,说明类是放在rt.jar包中。java提供Driver接口,厂商根据自己的需求实现功能。

双亲委派机制:当前类加载器收到类加载的请求后,先不自己尝试加载类,而是先将请求委派给父类加载器,调用父类的 loadClass() 方法,这是一个递归的过程(因此,所有的类加载请求,都会先被传送到启动类加载器),只有当父类加载器加载失败时,当前类加载器才会尝试自己去自己负责的区域加载。

所以判断是否破坏双亲委派机制的一个重要指标是类加载的请求顺序

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

下面是我们连接数据库常用的代码;

 String url = "jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL";
 String username = "root";
 String password = "root";
 String driverClassName = "com.mysql.cj.jdbc.Driver";
 // 注册驱动
 Class.forName(driverClassName);
 // 获取连接
 Connection connection = DriverManager.getConnection(url, username, password);
 System.out.println("数据库连接成功 --》" + connection);
复制代码

我们点进java的Driver接口,可以看到:

 package java.sql;
 ​
 import java.util.logging.Logger;
 ​
 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;
 }
 ​
复制代码

所以上面的方法都需要厂商来实现。

  • 第一步注册驱动,在DriverManager 类中实现

java 提供了DriverManager来管理各种驱动(Driver),点进看源码:

 public class DriverManager {
 ​
     // List of registered JDBC drivers
     // 存储已经注册的驱动,这是一个由 JUC 下 ReentrantLock 实现的可以高并发的 List ,
     // 如果想要注册驱动,就需要 调用 addIfAbsent 方法添加进 List。
     private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
 ​
     private final static  Object logSync = new Object();
 ​
     private DriverManager(){}
     
     // 注册驱动
     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);
 ​
     }
 }
复制代码

上面比较重要的就是 静态代码块 和 CopyOnWriteArrayList 。

这个时候并没有调用 registerDriver 进行驱动加载,而是 Class.forName() 触发加载MySQL驱动,下面就是MySQL的驱动,其中调用了DriverManager.registerDriver( new Driver() )进行驱动加载

 package com.mysql.cj.jdbc;
 ​
 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!");
         }
     }
 }
复制代码
  • 第二步,获取连接对象。
  private static Connection getConnection(
      String url, java.util.Properties info, Class<?> caller) throws SQLException {
      
      /**
      * 代码省略部分,获取类加载器
      */
      
      // 遍历 registeredDrivers List ,通过 密码 和 用户名 连接
      for(DriverInfo aDriver : registeredDrivers) {
          // If the caller does not have permission to load the driver then
          // skip it.
          if(isDriverAllowed(aDriver.driver, callerCL)) {
              try {
                  println("    trying " + aDriver.driver.getClass().getName());
                  Connection con = aDriver.driver.connect(url, info);
                  if (con != null) {
                      // Success!
                      println("getConnection returning " + aDriver.driver.getClass().getName());
                      return (con);
                  }
              } catch (SQLException ex) {
                  if (reason == null) {
                      reason = ex;
                  }
              }
 ​
          } else {
              println("    skipping: " + aDriver.getClass().getName());
          }
 ​
      }
      // ...
  }
复制代码
  • 上述过程并没有破坏双亲委派机制。

使用的类加载器和 调用类的加载器一样( ApplicationClassLoader)。

破坏双亲委派机制的情况

JDBC4.0之后使用spi机制才会破坏双亲委派机制

使用 SPI 在META-INF/services/java.sql.Driver寻找实现类 com.mysql.cj.jdbc.Driver,如下图

image.png
所以我们就不需要自己注册驱动了(需要 SPI 服务帮我们注册),连接的过程变为了

 String url = "jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL";
 String username = "root";
 String password = "root";
 ​
 Connection connection = DriverManager.getConnection(url, username, password);
复制代码

然而, DriverManager 是位于rt.jar包中的,类加载器是启动类加载器,com.mysql.jdbc.Driver肯定不在 <JAVA_HOME>/lib,那就无法加载MySQL的Driver,我们可以用应用程序类加载器来加载。所以java开发者的设计是,添加一个线程上下文类加载器(Thread Context ClassLoader),在启动类加载器中获取应用程序类加载器。 Thread.setContextClassLoaser() 设置线程上下文类加载器,如果创建线程的时候没有设置,会从父类继承一个,默认应用程序类加载器。

线程上下文类加载器是让父类加载器请求子类加载器完成类的加载,打破了双亲委派机制。

具体过程,在 DriverManager 的静态代码块对 Driver进行加载:

 public class DriverManager {
     static {
         loadInitialDrivers();
         println("JDBC DriverManager initialized");
     }
     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;
             }
         });
 ​
         //..
     }
 }
复制代码

其中最重要的是SPI 服务,ServiceLoader 查找META-INF/services/java.sql.Driver文件 ,那么我们在下面用driversIterator遍历的时候就会发现文件中的内容,例如,MySQL中的就是一行字符串 com.mysql.cj.jdbc.Driver

我们点进ServiceLoader.load()方法查看

     public static <S> ServiceLoader<S> load(Class<S> service) {
         ClassLoader cl = Thread.currentThread().getContextClassLoader();
         return ServiceLoader.load(service, cl);
     }
 ​
     public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
         ClassLoader cl = ClassLoader.getSystemClassLoader();
         ClassLoader prev = null;
         // 如果创建线程的时候没有设置,会使用父类加载器,上面已经设置,那么这里的类加载器就是线程上下文类加载器
         while (cl != null) {
             prev = cl;
             cl = cl.getParent();
         }
         return ServiceLoader.load(service, prev);
     }
复制代码

出现了我们前面说的内容,线程上下文类加载器,说明不符合双亲委派机制了,所以ServiceLoader拿到了 拿到了线程上下文类加载器。

接着看源码,ServiceLoader 自己实现了 Iterator ,叫 LazyIterator 。

 private class LazyIterator
         implements Iterator<S>
 {
     //..
     private boolean hasNextService() {
         //..
     }
 ​
     private S nextService() {
         if (!hasNextService())
             throw new NoSuchElementException();
         // META-INF/services/java.sql.Driver文件中的内容,即 `com.mysql.cj.jdbc.Driver`
         String cn = nextName;
         nextName = null;
         Class<?> c = null;
         try {
             // 使用线程上下文类加载器加载 实现类(com.mysql.cj.jdbc.Driver)
             c = Class.forName(cn, false, loader);
         } catch (ClassNotFoundException x) {
             fail(service,
                  "Provider " + cn + " not found");
         }
         //..
     }
     // hasNext()..调用hasNextService()
     
     // next()..调用nextService()
 ​
 }
复制代码

这里用代码说明了加载com.mysql.cj.jdbc.Driver驱动的时候是用的线程上下文类加载器。

总结

在 JDBC 4.0以后 ,开始支持使用 SPI 的方式来注册 Driver , 扫描 META-INF/services/java.sql.Driver文件,使用线程上下文类加载器,破坏了双亲委派机制。