双亲委派

当某个类加载器要加载某个类时,它会先将加载任务委托给父类加载器,一直递归到顶层,如果父加载器没有加载这个类,自己才会尝试加载这个类。

类加载器的类别

BootstrapClassLoader(启动类加载器)

该加载器由C++编写,负责加载rt.jarresources.jar 等核心库,可以通过Launcher.getBootstrapClassPath().getURLs() 来获取 BootstrapClassLoader 的加载路径。在Java中无法直接获取该加载器的引用和信息

ExtClassLoader(标准扩展类加载器)

ExtClassLoader 主要负责加载系统变量java.ext.dirs 中的类,如果不指定,则默认为加载路径为 %JAVA_HOME%/jre/lib/ext。该加载器实现路径位于 sun.misc.Launcher$ExtClassLoader。父加载器是BootstrapClassLoader(由C++实现)

AppClassLoader(系统类加载器)

负责加载classpath下的类,父加载器是ExtClassLoader,实现路径位于sun.misc.Launcher$AppClassLoader

双亲委派的作用

  • 避免重复加载,父加载器加载过的类,子加载器无需再加载
  • 避免核心类库被篡改

JDBC与破坏双亲委派

JDBC之所以要破坏双亲委派模式是因为,JDBC的核心在rt.jar中由启动类加载器加载,而其实现则在各厂商实现的的jar包中,根据类加载机制,若A类调用B类,则B类由A类的加载器加载,也就是说启动类加载器要加载jar包下的类,我们都知道这是不可能的,启动类加载器负责加载%JAVA_HOME%/jre/lib/中的一些类,那么JDBC是如何加载这些Driver实现类的?

测试代码

以下是两个不同方式连接mysql数据库(需预先导入mysql驱动)

方式一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.mysql.cj.jdbc.Driver;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class ConnectionTest1 {
public static void main(String[] args) {
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "123456");
try {
Connection con = new Driver().connect("jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai", info);
System.out.println("连接成功");
} catch (SQLException e) {
e.printStackTrace();
}
}
}

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionTest2 {
public static void main(String[] args) {
try {
DriverManager.getConnection("jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai",
"root", "123456");
System.out.println("连接成功");
} catch (SQLException e) {
e.printStackTrace();
}
}
}

方式一和方式二最明显的区别是:方式一显式地指定了com.mysql.cj.jdbc.Driver类为数据库连接驱动,而方式二并未指定连接驱动。

源码分析

com.mysql.cj.Driver

当我们加载数据库驱动类的时候,驱动类一般会将自己主动注册到注册表中,这里以com.mysql.cj.Driver为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

public Driver() throws SQLException {
}
}

在早期的JDBC版本中,我们常看到这样一种写法:Class.forName("com.mysql.Driver"),通过手动加载驱动类,来实现驱动注册,JDBC3.0之后的版本中已经不需要这样手动加载驱动了。

DriverManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// JDBC驱动注册表
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {

// 省略若干代码

for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

// 省略若干代码
}

在测试代码的方式二中,调用了DriverManager#getConnection方法。该方法的核心逻辑是遍历JDBC驱动注册表中注册的驱动,尝试连接,若某个驱动连接成功,则直接返回连接。

问题:JDBC3.0之后是何时加载的驱动类

针对这个问题我们继续分析 DriverManager

1
2
3
4
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

DriverManager 中有这样一段静态代码块,调用了 loadInitialDrivers 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private static void loadInitialDrivers() {
// 省略
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
}
return null;
}
});
// 省略
}

通过loadInitialDrivers方法中,通过 ServiceLoader 加载了满足一定条件的 java.sql.Driver 的实现类,这就是Java提供的SPI机制

SPI机制

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 classpath 路径下的META-INF/services文件夹查找文件,该文件的文件名为被实现类的全限定名,自动加载文件里所定义的类。

首先看看 mysql-connector-java.jar 是如何利用SPI实现驱动加载的

该文件的内容只是 java.sql.Driver 的实现类

1
com.mysql.cj.jdbc.Driver

ServiceLoader

DriverManager#loadInitialDrivers 调用了 ServiceLoader 的迭代器 LazyIterator

该迭代器的 hasNext 核心实现在 hasNextService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 获取 META-INF/servers/路径下对应需要被加载的类的文件名,对应上面的java.sql.Driver文件
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 真正需要被加载的实现类,对应上面文件的内容 “com.mysql.cj.Driver”
nextName = pending.next();
return true;
}

该迭代器的next 方法核心实现为 nextService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
// hasNextService 方法结尾中获取的需要被加载的类
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 这里尝试加载,但不执行static代码块
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
// 判断文件中的类是否为文件名所代表的类的实现类。
// 例如上述文件中的com.mysql.cj.Driver必须为java.sql.Driver的实现类,否则失败
// 为此,上面并没有直接执行static代码块
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
// 这里开始执行静态代码块
// com.mysql.cj.Driver 从这之后开始注册
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service, "Provider " + cn + " could not be instantiated", x);
}
throw new Error(); // This cannot happen
}

结论

DriverManager 利用了 SPI 机制,加载驱动类,驱动类将自己注册到了注册表中,通过遍历注册表获取连接

由于各个厂商的驱动类无法直接通过 BootstrapClassLoader 加载,因此需要将加载的任务委托给线程上下文的加载器