大家好,我是大明哥,一个专注 「死磕 Java」 的硬核程序员。
回答
在 JDBC 4.0之后使用spi机制才会破坏双亲委派机制。
java.sql.DriverManager 在初始化时会调用 ServiceLoader 的 load() 去加载实现了java.sql.Driver接口的实现类。ServiceLoader 会搜索当前类路径上所有JAR文件内的META-INF/services/java.sql.Driver文件,从这些文件中读取实现类的全限定名。假如我们加载到了 MySQL 的驱动程序,获取的实现类为 com.mysql.jdbc.Driver,但是 DriverManager 位于 rt.jar 包,类加载器是启动类加载器,而 com.mysql.jdbc.Driver 位于 MySQL 驱动程序的 jar 文件中,启动类加载器是无法加载该类的,那怎么办呢?
添加一个线程上下文类加载器(Thread Context ClassLoader),在启动类加载器中获取应用程序类加载器。有了线程上下文类加载器,应用程序就可以把原本需要由启动类加载器进行加载的类,改为由应用程序类加载器来加载了,从而打破双亲委派模型。
详情
原生 JDBC 中的 Driver 驱动仅仅只是一个接口,具体的实现有不同的数据库厂商来提供,例如 MySQL 数据库提供的 com.mysql.cj.jdbc.Driver。
DriverManager 是一个管理 JDBC 驱动的类,它主要提供两个功能:
- 驱动注册:当 JDBC 驱动类被加载到JVM时,它应该调用
DriverManager.registerDriver()将自身注册到 DriverManager 中。 - 管理连接:调用
DriverManager.getConnection(url, user, password)可以获取与数据库的连接。这个方法会检查已注册的驱动,找到能够处理提供的URL的合适驱动,并通过该驱动建立连接。
未破坏双亲委派机制的情况
下面这段代码是我们连接数据库最基本的用法:
String url = "jdbc:mysql://localhost:3306/skjava";
String username = "root";
String password = "123456";
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
Connection connection = DriverManager.getConnection(url, username, password);
我们显示使用 Class.forName("com.mysql.cj.jdbc.Driver"); 的方式来进行驱动的加载,类加载器是使用的调用当前方法所用的类加载器,这个过程是遵循双亲委派模型的。
破坏双亲委派机制的情况
注:下面例子以 MySQL 为例。
从 JDBC 4.0 开始,引入了基于服务提供者接口(SPI)的方式来注册 Driver。通过这种方式我们就不需要显示调用 Class.forName() 来加载驱动程序了。
具体做法是,在 MySQL jar 包中的META-INF/services/java.sql.Driver 文件中会指明实现 Driver 接口的实现类是哪一个,例如:
在使用的时候我们就不需要显示调用 Class.forName("com.mysql.cj.jdbc.Driver") 来指明 MySQL 的驱动类了,由 DriverManager 自动发现。
DriverManager 类在初始化时会调用 loadInitialDrivers() 来初始化驱动程序:
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
// ....
}
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;
}
});
//...
}
ServiceLoader 会扫描当前类路径上所有JAR文件内的META-INF/services/java.sql.Driver文件,从这些文件中读取实现类的全限定名。然后迭代(ServiceLoader 实现了 Iterable 接口,其本身就是一个迭代器)。这里调用 driversIterator.next() 是 ServiceLoader 中的
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
我们一路跟进会到这里:
private S nextService() {
//...
try {
// cn就是数据库厂商提供的类名类名
// loader 就是类加载器
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
//...
throw new Error(); // This cannot happen
}
loader 是加载 cn 的类加载器。我们知道DriverManager 位于 rt.jar 包,它类加载器是启动类加载器,而 com.mysql.jdbc.Driver 位于 MySQL 驱动程序的 jar 文件中,是无法通过启动类加载器来加载的。所以它由 loader 来加载,那这个 loader 是在什么时候指定的?构造 ServiceLoader 对象的时候:
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}
那什么时候构造 ServiceLoader 的呢?DriverManager#loadInitialDrivers() 中:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器去加载驱动类
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
从这里我们可以看出加载 com.mysql.jdbc.Driver 的加载器是通过 Thread.currentThread().getContextClassLoader()来获取的,这里获取的是应用程序类加载器。
所以,由于DriverManager 无法加载 MySQL jar 中的驱动程序,我们通过线程上下文类加载器拿到应用程序类加载器,同时也在 MySQL 的 jar 中获取到了具体的驱动实现类,这样我们就可以成功地在 rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了,从而打破双亲委派模型。
总结下:
- 从
META-INF/services/java.sql.Driver文件中获取java.sql.Driver接口的实现类名 "com.mysql.jdbc.Driver"。 - 通过
Thread.currentThread().getContextClassLoader()获取线程上下文类加载器,从而获取到应用程序类加载器。 - 通过线程上下文类加载器去加载
com.mysql.jdbc.Driver,从而避开了双亲委派模型的弊端。
本文已收录到大明哥的「 Java 面试宝典」中。
💻 死磕 Java