美文网首页
数据库问题:通过 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