美文网首页
详细说明为什么 JDBC 不用写 Class.forName()

详细说明为什么 JDBC 不用写 Class.forName()

作者: 马飞凌凌漆 | 来源:发表于2022-04-29 13:35 被阅读0次

为什么要写 Class.forName

先看下为什么以前要写 Class.forName("com.mysql.cj.jdbc.Driver")

学了类加载流程后知道 Class.forName() 是将某个包下的类进行 ”加载-连接-初始化“,其中初始化阶段就是执行类的初始化方法,也就是 static 代码块里的方法,看下 com.mysql.cj.jdbc.Driver 的类初始化方法做了什么

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

Driver 将 new 了一个自己,并传递给了 DriverManager.registerDriver() 方法,DriverManager 这个方法中:

public static void registerDriver(java.sql.Driver driver, DriverAction da)throws SQLException {
    if (driver != null) {
        registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
    } else {
        throw new NullPointerException();
    }
}

将 Driver 添加到了 registeredDrivers 里,registeredDrivers 是一个线程安全的 ArrayList:

private static final CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

registeredDrivers 怎么是个数组?

因为系统中可能既用到了 MySQL 数据库也用到了 Oracle 数据库,或者还有其他数据库,这些数据库的驱动,也就是 Driver 类,实现都是不一样的,所以使用个数组将加载的所有驱动都存起来。

也就是说执行了 Class.forName("com.mysql.cj.jdbc.Driver") 后,DriverManager 就拿到了 com.mysql.cj.jdbc.Driver对象,存到内部的一个数组中。

获取数据库连接流程

再看一下我们是怎么获取数据库连接的,一段标准的 JDBC 代码:

Connection conn = DriverManager.getConnection(url, username, password);
String sql = "select * from user";
PreparedStatement prep = conn.prepareStatement(sql);
// do something..

其中 DriverManager.getConnection() 的方法最后会调用这个重载方法:

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    // .... 代码做了精简

    // 枚举 registeredDrivers 这个 ArrayList
    for(DriverInfo aDriver : registeredDrivers) {
        
        // aDriver.driver 就是 com.mysql.cj.jdbc.Driver
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            Connection con = aDriver.driver.connect(url, info);
            if (con != null) {
                return con;
            }
        }
    }
    throw new SQLException("No suitable driver found for "+ url, "08001");
}

DriverManager.getConnection() 会从所有的注册的数据库驱动中尝试连接到目标 URL,调用它的 Driver.connect(url, info) 方法拿到数据库连接,返回给调用者。只有 MySQL 使用 com.mysql.cj.jdbc.Driver.connect(url, info), Oracle 使用 oracle.jdbc.driver.OracleDriver.connect(url, info) 才能连接成功。

结论

也就是说调用 DriverManager.getConnection() 就是调用 com.mysql.cj.jdbc.Driver.connect() 。
如果没有执行 Class.forName("com.mysql.cj.jdbc.Driver") 将 MySQL 的驱动注册给 DriverManager 的话,DriverManager 内部的 ArrayList<Driver>就是空的,执行DriverManager.getConnection() 就什么也得不到。

为什么 JDBC 现在不用写 Class.forName() 加载驱动

不写 Class.forName("com.mysql.cj.jdbc.Driver") 也能获取到 Connection,说明 MySQL 的 Driver 驱动类自己自动注册到了 DriverManager 内部的数组中,有东西帮我们执行了:Class.forName("com.mysql.cj.jdbc.Driver") ,怎么做到的呢?

我们在执行:DriverManager.getConnection(url, username, password) 时,会触发 DriverManager 的类加载流程,也就是会执行 DriverManager 的类初始化方法,看下这个方法:

// 源码做了简化调整
static {
    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;
        }
    });
    // ----------------------------分割线------------------------------
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    for (String aDriver : driversList) {
        try {
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
        }
    }
}

先看分割线下面的部分,它会从系统属性 "jdbc.drivers" 中获取数据库驱动 Driver 的全限定名,使用 : 冒号分割,如果系统中同时使用了 MySQL 和 Oracle,那就可以在启动时加上属性:

jdbc.drivers = com.mysql.cj.jdbc.Driver:oracle.jdbc.driver.OracleDriver

接着会执行:

Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());

也就是在这里帮我们执行了 Class.forName("com.mysql.cj.jdbc.Driver")

但是平时我们写 JDBC 程序时也没有加这个参数啊,它又是怎么自动注册的呢?

ServiceLoader

SPI

先说下 SPI 服务提供者接口(Service Provider Interface)(更具体的涵义查阅其他资料)

我们写的 JDBC 代码都是按照 JDK 提供的接口进行编程的,比如 DriverManager、Driver、Connection、PreparedStatement、ResultSet,它们都是 java.sql 下的包,也就是 JDK 提供的。

这些接口具体的实现类是要由数据库厂商自己写的,我们只需要面向接口编程,而不用直接操作实现类。

在JDBC 环境下,SPI 就是各个 MySQL、Oracle 等数据库厂商提供的 Driver 实现类。
ServiceLoader 就是用来实现服务提供者接口 SPI 的加载


再看分割线上面的部分:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(java.sql.Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 加载驱动程序,并执行类加载的整个流程:加载-连接-初始化
try{
    while(driversIterator.hasNext()) {
        driversIterator.next(); //执行Class.forName("com.mysql.cj.jdbc.Driver")
    }
} catch(Throwable t) {
    // Do nothing
}

以上代码就是在所有加载的 jar 包中(包括启动类加载器和扩展类加载器加载的 jar 包)搜索 META-INF/services 文件夹,看这个文件夹下面有没有java.sql.Driver 的文件,有就将文件里的类加载到内存中并初始化,初始化就意味着会执行类初始化方法。

ServiceLoader 是在使用 LazyIterator 迭代的时候完成类的初始化,内部使用 newInstance() 创建了一个 Driver 实例对象来进行类初始化,而不是 Class.forName

总结

不用写 Class.forName() 是因为使用了 SPI 机制,由 ServiceLoader 加载 com.mysql.cj.jdbc.Driver,
ServiceLoader 帮我们执行了 Class.forName("com.mysql.cj.jdbc.Driver"),触发类加载流程,执行 Driver 的类初始化。

Driver 的类初始化方法中将自己注册给了 DriverManager,
问 DriverManager 要 Connection 就是调用它加载的驱动程序 Driver.connect(),
由 Driver.connect() 拿到 Connection

相关文章

网友评论

      本文标题:详细说明为什么 JDBC 不用写 Class.forName()

      本文链接:https://www.haomeiwen.com/subject/iidsyrtx.html