写点什么

深入理解 ContextClassLoader

用户头像
NORTH
关注
发布于: 2020 年 06 月 02 日
深入理解ContextClassLoader

类的加载器大多都遵循双亲委托机制,它不仅可以避免一个类被重复加载,也能够避免Java核心类被篡改,越基础的类由越上层的类加载器加载。这些核心类之所以重要,是因为它们往往被用户代码继承和调用。但程序设计往往没有绝对不变的规则,如果有核心类要回调用户的代码,那该怎么办?



一些典型的例子,如JNDI服务、JDBC等,它们都是Java的标准服务,它们的代码都是由启动类加载器来完成加载的,但它们的需要调用其它厂商的实现才能提供对应的服务。以JDBC为例,Java应用在连接数据库时,都需要调用以下代码来向驱动管理器注册对应数据库的驱动:

/**
* 向DriverManager注册指定的驱动程序
*/
DriverManager.registerDriver(java.sql.Driver driver)



透过前面的文章知道,一个类都会尝试使用自己的类加载器(即加载自身的类加载器)去加载其所依赖的类,比如A类引用了B类,那么加载A的类加载器就会尝试去加载B(前提是B尚未加载)。而在这儿,DriverManager是Java核心类,由启动类加载器加载,而Driver则是由第三方厂商根据JDBC标准来实现的,启动类加载器是绝不可能认识并加载这些类的,那该怎么办?



为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。它是从JDK1.2开始引入的,可以通过Thread类的getContextClassLoader()setContextClassLoader(ClassLoader cl)来获取和设置当前线程的类加载器。如果创建线程时没有设置,它将继承其父线程的,如果应该程序在全局范围内都没有设置过的话,那这个类加载器默认是系统类加载器(AppClassLoader)。



有了上下文加载器,应用就可以破坏类的双亲加载机制了,还是以JDBC为例,看看DriverManager类中如何获取数据的连接的。

//代码摘自:java.sql.DriverManager
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {
// Reflection.getCallerClass()
// 返回调用此方法的方法的调用类的类型,即:Class<?>
return (getConnection(url, info, Reflection.getCallerClass()));
}

注意代码中的Reflection.getCallerClass(),然后再往下看:

private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application’s
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
callerCL = Thread.currentThread().getContextClassLoader();
}
if (url == null) {
throw new SQLException(“The url cannot be null", "08001");
}
println(“DriverManager.getConnection(\”” + url + “\”)”);
ensureDriversInitialized();
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
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());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

首先获取调用类的类加载器,如果没有传调用类,那么就使用当前线程的上下文加载器。而这个类加载器用来干什么?在看下isDriverAllowed的源码:

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if (driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false;
}
return result;
}

在使用DriverManager之前都需要先注册驱动,即isDriverAllowed方法的driver参数获取数据库连接前注册的驱动,这个方法中使用获取的类加载器再重新初始化一次Driver类的目的是什么?其实是为了确保事先注册的驱动与当前的驱动是通过同一个类加载器加载的。



JDBC通过这种方式来使用线程上下文类加载器去加载所需的服务代码,这种由父类加载器去请求自类加载器完成类加载行为,实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派机制的一般性原则。



最后,除了SPI等场景外,个人不太建议在业务场景使用使用ContextClassLoader,否则出现问题后,不太容易排查和解决。



深入理解JVM系列的第3篇,从目录阅读请移步:深入理解JVM系列文章目录

参考资料

周志明著;深入理解Java虚拟机(第三版);机械工业出版社;2019-12



发布于: 2020 年 06 月 02 日阅读数: 125
用户头像

NORTH

关注

Because, I love. 2017.10.16 加入

这里本来应该有简介的,但我还没想好 ( 另外,所有文章会同步更新到公众号:时光虚度指南,欢迎关注 ) 。

评论

发布
暂无评论
深入理解ContextClassLoader