美文网首页
详细说明为什么 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