传统方法

使用Java代码连接MySQL需要走以下流程(使用框架也要做对应的配置):

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws ClassNotFoundException, SQLException {
String url = "jdbc:mysql://localhost:3306/db";
String username = "root";
String password = "root";
String driverClassName = "com.mysql.cj.jdbc.Driver";
// 注册驱动
Class.forName(driverClassName);
// 获取连接
Connection connection = DriverManager.getConnection(url, username, password);
}

这里边涉及到一个问题,为什么一定要写一句Class.forName(driverClassName)

这里首先涉及到Java的类加载机制

想要使用一个类,则必须要求该类已经被加载到JVM中,加载的过程实际上就是通过类的全限定名来获取定义该类二进制字节流,然后将这个字节流所表示的静态存储结构转换为方法去的动态运行时数据结构。同时在在内存中实例化一个java.lang.Class对象,作为方法区中该类的数据访问入口(供我们使用)。

​ —— 出自《深入理解Java虚拟机》

其实在一开始,我并不了解Class.forName()是干嘛的,后边了解到它用作加载类,官方解释为:在运行时动态的加载一个类,返回值为生成的Class对象。所以这行代码的目的,就是将com.mysql.cj.jdbc.Driver类加载到Jvm中了。

这里,forName方法的具体实现如下:

1
2
3
4
5
6
7
8
9
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
// 注意这个true,该参数用来标识在将该类加载后是否进行初始化操作。
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

private static native Class<?> forName0(String name, boolean initialize,ClassLoader loader,Class<?> caller);

到这里,也就是说这一行代码不仅加载了对应的类,也做了初始化操作。

至于说后续为什么可以直接在DriverManager使用,就要看Driver类里面实现了什么。

MySQL的驱动实现了Java官方提供的Driver接口,这也是每一个数据库厂商所必须要做的事情。而且他们都需要以下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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!");
}
}
}

该类中定义了一个静态代码块,静态代码快中创建了一个驱动类实例注册给了DriverManager,而静态代码块的内容会在初始化的过程中执行,所以才能通过DriverManager.getConnection直接获取一个连接。

打破双亲委派机制

在jdbc4.0之后,使用了spi机制,破坏了双亲委派机制。也就是说我们不再需要写哪一行Class.forName(driverClassName);

我们只需要将对应的驱动类的jar包放到工程的class path下,驱动类会 自动被加载。

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。

SPI的目的是为了提前使用某些未被实现的方法。定义一组接口,然后直接通过接口使用它的方法,但是这些方法还未被实现,留给第三方去实现,这就是spi的目的。

还有一种说法,SPI,为了解耦,从配置里获取某个接口的具体实现类。

为了支持这个新特性,各个数据库厂商的jar包都有一个META-INF/services目录,里面有一个java.sql.Driver,这里指定了driver的全限定名。

image-20231102102705989

存在的问题

JDBC的driver接口是定义在JDK中的,但是它的实现类,确在一个jar包中,放在classpath下。就存在以下问题:

  • DriverManager类会加载每个Driver接口的实现类并管理它们,但是DriverManager类自身是 jre/lib/rt.jar 里的类,是由bootstrap classloader加载的。
  • 根据类加载机制,某个类需要引用其它类的时候,虚拟机将会用这个类的classloader去加载被引用的类,但是bootstrap classloader是无法加载这个driver的(bootstrap classloader只能加载Java 的核心类库包)。
  • 因此只能在DriverManager里强行指定下层classloader来加载Driver实现类,而这就会打破双亲委派模型。

具体的做法是,添加了一个线程上下文类加载器Thread Context ClassLoader,在启动类加载器中获取应用程序类加载器。Thread.setContextClassLoaser() 设置线程上下文类加载器,如果创建线程的时候没有设置,会从父类继承一个,默认应用程序类加载器。