美文网首页
数据库问题:通过 Loading `class com.mysq

数据库问题:通过 Loading `class com.mysq

作者: RunAlgorithm | 来源:发表于2019-02-05 12:49 被阅读8次

    这是个很简单的问题,解决方案只要把引起主动加载 com.mysql.jdbc.Driver 的代码去掉。

    不过这是个很好的例子去理解类加载机制与 Java SPI 的实现。

    1. 场景

    先看触发这个问题的一个比较原始的场景:我们直接调用 JDBC 进行数据库查询。

    1.1. 问题代码

    使用的 mysql 驱动的版本为 6.0.6:

        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>6.0.6</version>
        </dependency>
    

    查询代码如下:

        public static void main(String[] args) {
            // 加载类
            try {
                Class.forName("com.mysql.jdbc.Driver");
                Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
                PreparedStatement pstmt = conn.prepareStatement(SQL);
                ResultSet rs = pstmt.executeQuery();
                ...
            } catch (SQLException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    

    我们目前用到的一些 ORM 库,底层都是使用 JDBC 进行数据库操作的,可以认为它们是对 JDBC 的进一步封装,为数据库操作加入面向对象的特性。ORM 实际上就是关系-对象映射。

    要能操作数据库,还是离不开各大数据库生厂商提供的驱动。

    回到基础的 JDBC 调用,一共为 5 大步骤:

    • 加载驱动
    • 建立数据库连接 Connection
    • 创建执行 SQL 语句的 Statement
    • 处理执行结果 ResultSet
    • 释放资源。

    虽然我们知道了结果,是那个地方发出了警告,

    但为了探索原因,或者在原因尚不明确的情况下,还是要再问一句 Why ?

    是哪个过程的警告?

    1.2. 完整警告

    这次控制台报出的完整警报如下:

    Loading class `com.mysql.jdbc.Driver'. This is deprecated. 
    The new driver class is `com.mysql.cj.jdbc.Driver'. 
    The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
    

    内容可以归纳为:

    • com.mysql.jdbc.Driver 已经过时,现在用的是 com.mysql.cj.jdbc.Driver
    • 该驱动已经通过 SPI 机制自动注册,不需要手动添加

    虽然有警告,程序运行完全正常,没有影响。

    即便如此,不能抱有侥幸心理,还是需要探究原因,顺便巩固一下 Java 和 Jvm 的知识。

    2. 原因探索

    根据警告的线索,先查 com.mysql.jdbc.Driver 类。

    可以得知,mysql-connector-java 的 6.0.6 版本已经废弃了 com.mysql.jdbc.Driver,改用 com.mysql.cj.jdbc.Driver。

    查看驱动的 jar 文件后确实如此。

    这里是旧版的 com.mysql.jdbc.Driver:

    package com.mysql.jdbc;
    
    import java.sql.SQLException;
    
    public class Driver extends com.mysql.cj.jdbc.Driver {
        public Driver() throws SQLException {
        }
    
        static {
            System.err.println("Loading class `com.mysql.jdbc.Driver\'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver\'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
        }
    }
    

    该废弃的 Driver 实际是 com.mysql.cj.jdbc.Driver 的子类,拥有完整的驱动实现。

    可以看到警告信息在 static 语句块中。

    保留 com.mysql.jdbc.Driver 的原因应该是为了和一些旧项目的代码进行兼容,确保升级 mysql 驱动后程序的稳定性。

    然后通过 static 代码块的警告来提示使用者,需要调整代码了。

    2.1. 谁触发了 static 语句?

    根据对类的知识,我们知道 static 语句一般发生在类加载中。

    然而类加载过程又是复杂的,可以大致分为三个阶段

    • 加载。
    • 连接,包括验证、准备、解析。
    • 初始化。

    而初始化就是 static 语句执行的时机。

    从资料中找到了整个类声明周期的图,包括了它的加载过程:

    image

    实际上,类加载到内存中,不一定会马上引起初始化。

    也就是经过了加载、连接后,初始化不一定会执行。

    这一块 JVM 规范做出来很强硬的约束。只有对类的主动引用引起的类加载,初始化才会被马上触发,这时候 static 语句才会执行。

    JVM 规范实际上只给出了 5 个场景,有且只有这 5 个场景会去触发初始化。以下五个场景摘自周志明的 《深入理解 Java 虚拟机》第 2 版的描述:

    1. 遇到 new、getStatic、putStatic 或者 invokestatic 这 4 条字节码指令时,如果类没有进行初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
    3. 当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
    4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。
    5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

    也就是不满足这五个场景,及时类完成了加载和连接过程,也不会执行初始化。

    是的,就是如此的粗暴。

    然后有例子吗?有的,比如下面的程序:

    public class TestDriverInit {
    
        public static void main(String[] args) {
    
            currentClasses();
    
            System.out.println("调用 Driver.class :" + Driver.class + "\n");
    
            currentClasses();
        }
    
        private static void currentClasses() {
            System.out.println("当前加载好的类:");
            ClassLoader loader = Thread.currentThread().getContextClassLoader();
            try {
                Field clsField = ClassLoader.class.getDeclaredField("classes");
                clsField.setAccessible(true);
                Vector<Class<?>> classes = (Vector<Class<?>>) clsField.get(loader);
                for(Class c : classes) {
                    System.out.println(c);
                }
            } catch (IllegalAccessException | NoSuchFieldException e) {
                e.printStackTrace();
            }
            System.out.println();
        }
    }
    

    为了验证类是否被加载,我们采用反射的方法输出了 ClassLoader 的 classes 成员变量:

     public abstract class ClassLoader {
        ...
        private final Vector<Class<?>> classes = new Vector<>();
        ...
    }
    

    编译执行代码后,我们从控制台中发现了这样的输出:

    当前加载好的类:
    class com.intellij.rt.execution.application.AppMain
    class com.intellij.rt.execution.application.AppMain$1
    class steven.lee.jdbc.TestDriverInit
    
    调用 Driver.class :class com.mysql.jdbc.Driver
    
    当前加载好的类:
    class com.intellij.rt.execution.application.AppMain
    class com.intellij.rt.execution.application.AppMain$1
    class steven.lee.jdbc.TestDriverInit
    class com.mysql.cj.jdbc.NonRegisteringDriver
    class com.mysql.cj.jdbc.Driver
    class com.mysql.jdbc.Driver
    

    通过打印 ClassLoader 已经加载的类的列表,我们知道这个 Driver 确实加载了。

    然而,那个 Driver 的 static 语句块并没有执行,警告也没有被触发。

    意料之外情理之中的结果。

    我们的 Driver.class 使用,不属于 JVM 规范中的 5 点的任何之一。它不属于主动引用,而是对类的被动引用。

    那初始化最终会触发吗?会的,等主动引用的 5 个场景之一发生了后,自然就触发了 static 语句块。

    那么回到我们的问题代码,哪里符合了初始化 5 场景?

    按照 JDBC 流程,我们第一步使用 Class.forName 方法对 mysql 驱动库的 Driver 类进行加载:

      Class.forName("com.mysql.jdbc.Driver");
    

    就是这一步触发了警报。

    这里属于第 2 个场景,反射的使用,属于是对 Driver 的主动引用,触发了 static 。

    知道了警告的触发后,我们接着思考下一个问题:该驱动已经通过 SPI 机制自动注册,不需要手动添加

    2.2. 什么时候自动注册驱动

    要理解驱动自动注册的逻辑,需要理清楚两个问题:

    • 注册的代码在哪里
    • 怎么调用的

    2.2.1. 注册的代码

    我们知道了 com.mysql.jdbc.Driver 被 com.mysql.cj.jdbc.Driver 代替,于是我们在 jar 包中找到了它:

    package com.mysql.cj.jdbc;
    
    import com.mysql.cj.jdbc.NonRegisteringDriver;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    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!");
            }
        }
    }
    

    com.mysql.cj.jdbc.Driver 在 static 语句块中调用了 DriverManager.registerDriver(new Driver()) 进行了注册。

    com.mysql.cj.jdbc.Driver 继承于 NonRegisteringDriver。

    NonRegisteringDriver 才是 JDBC 规范的 java.sql.Driver 的实现的主体。

    所以,注册的代码就在新的驱动类 com.mysql.cj.jdbc.Driver 的 static 代码块中。

    通过上面我们对虚拟机的了解,com.mysql.cj.jdbc.Driver 必须满足上述的 5 个主动引用的场景,才会执行 static 代码块,驱动注册过程才能执行。

    什么时候调用呢,我们看到了另一个关键词 Java SPI

    2.2.2. 调用的时机

    Java 的 SPI 全称 Service Provider Interface,源自服务提供者框架 Service Provider Framework。

    是用来进行接口与服务实现分离的目的。

    这样子,我们只需要面向接口编程,具体的实现可以通过配置来更换,解耦得很干净,同时因为面向接口编程,不需要因为实现的改变而修改代码。

    这个也是很多 Java 框架的基础。

    系统使用 ServiceLoader 来执行,按照规范会有个配置文件放在 META-INF 目录下。这是这个配置文件决定了要加载哪一个实现。

    JDBC 也是基于这个实现的。

    配置文件在 jar 包可以找到:

    image

    就是 java.sql.Driver,文件内容正是新的驱动类 com.mysql.cj.jdbc.Driver。

    那么 JDBC 什么时候会使用 ServiceLoader 去加载 java.sql.Driver 呢?

    答案就在 DriverManager 中。查 DriverManager 的代码看到:

    public class DriverManager {
    
        ...
    
        /**
         * Load the initial JDBC drivers by checking the System property
         * jdbc.properties and then use the {@code ServiceLoader} mechanism
         */
        static {
            loadInitialDrivers();
            println("JDBC DriverManager initialized");
        }
        
        ...
     }
    

    在静态代码块中有 loadInitialDrivers 方法,根据语意应该是要加载初始化驱动。

    我隐藏了一些细节代码,把重要步骤提现出来:

    private static void loadInitialDrivers() {
        ...
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);     
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        ...
        while(driversIterator.hasNext()) {
            driversIterator.next();
        }
        ...
        for (String aDriver : driversList) {
            ...
            Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
            ...
        }
    }
    

    所以,在 DriverManager 的类加载过程的初始化阶段,就会去注册驱动。

    而实现的方式正式 Java SPI 机制,和上述警告发出的内容一致。

    我们不需要再主动调用 Class.forName 去加载驱动到内存,并完成它的初始化,在下面这一句自然会发生:

    Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
    

    也就是创建数据库连接的时候:

    • 如果是第一次调用 DriverManager,触发类加载机制
    • DriverManager 这里属于主动引用,如果未初始化,会执行类加载机制中的初始化过程
    • 在初始化中,static 代码块会被执行
    • static 代码块会调用 SPI 机制,从 mysql 驱动的 jar 包的 META-INF 中读取 java.sql.Driver 的实现类
    • 加载类进入内存,完成注册

    3. 总结

    正如文章开头说的一样,这个问题很好解决,直接把 JDBC 的加载驱动步骤省去即可。

    Class.forName("com.mysql.jdbc.Driver");
    

    但这篇文章的目的更多地是做一个延伸,对 JVM 类加载的初始化过程,还有 SPI 机制有了一个更具体的体验。

    相关文章

      网友评论

          本文标题:数据库问题:通过 Loading `class com.mysq

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