深入理解 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) {
    // Reflection.getCallerClass() 返回调用此方法的调用类的类型,即:Class<?>
    return (getConnection(url, info, Reflection.getCallerClass()));
}

注意代码中的 Reflection.getCallerClass() ,它回返回调用此方法的调用类的类型信息,即 Class<?>。然后再往下看:

// 省略部分判断代码,仅保留核心逻辑
private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) {
    // callerCL 为调用类的类加载器,即调用getConnection方法的类的类加载器
    // 如果 callerCL 为空,则使用线程上下文类加载器
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
        callerCL = Thread.currentThread().getContextClassLoader();
    }

    ensureDriversInitialized();
    
    // 遍历已注册的驱动程序,尝试建立连接,并记录首个异常 
    SQLException reason = null;
    for (DriverInfo aDriver : registeredDrivers) {
        // 如果调用方没有加载驱动程序的权限,则跳过它。
        if (isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                Connection con = aDriver.driver.connect(url, info);
                if (con != null) {
                    return (con);
                }
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }
        } else {
            println("skipping: " + aDriver.getClass().getName());
        }
    }
    // 所有注册驱动均无法建立连接
    if (reason != null)    {
        throw reason;
    }
}

首先获取调用类的类加载器,如果没有传调用类,那么就使用当前线程的上下文加载器。用获取到的类加载器来判断是否有权限加载已经注册的驱动程序:

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 系列
本站为非交互式网站,根据相关规定,你无法在本站内进行评论,如对本文有任何疑问,可移步到微信公众号评论
在微信客户端内打开右侧链接即可评论:深入理解 ContextClassLoader
不遗漏重要文章,欢迎关注本站公众号,手机扫描下方二维码或微信搜索 橙子成绩好
微信公众号二维码

回到首页