• 欢迎访问天天编码网站,Java技术、技术书单、开发工具,欢迎加入天天编码
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏天天编码吧
  • 我们的淘宝店铺已经开张了哦,传送门:https://shop145764801.taobao.com/

真正理解线程上下文类加载器

Java高级 tiantian 299次浏览 0个评论 扫描二维码

前言

此前我对线程上下文类加载器(ThreadContextLoader)的理解仅仅局限于下面这段话:

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由引导类加载器来加载的;SPI的实现类是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

一直困恼我的问题就是,它是如何打破了双亲委派模型?又是如何逆向使用类加载器了?直到今天看了jdbc的加载过程才茅塞顿开,其实挺简单的,只是一直没去看代码导致理解不够到位。

JDBC案例分析

先来看下JDBC的定义,JDBC(Java Data Base Connectivity)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建更高级的工具和接口,使数据库开发人员能够编写数据库应用程序。

也就是说JDBC就是java提供的一种SPI,要接入的数据库供应商必须按照此标准来编写实现类。

jdk中的DriverManager的加载Driver的步骤顺序依次是:

  1. 通过SPI方式,读取META-INF/services下文件的配置,使用线程上下文类加载器加载
  2. 通过System.getProperty(“jdbc.drivers”)获取设置,然后通过系统类加载器加载
  3. 用户调用Class.forName()加载到系统类加载器,然后注册到DriverManager对象,在需要使用时直接取用,但需校验注册和调用的类加载是否一样,校验这步需要借助线程上下文类加载器(否则多模块应用同时都注册了mysql driver,从DriverManager取时将出现错乱)

步骤1和3都是用了线程上下文类加载器,1中的SPI方式与案例2中的类似,这儿先略过,着重讲下3中使用线程上下文类加载器校验的方法。

步骤1和2的代码在初始化方法loadInitialDrivers()中:

<code class="hljs cs has-numbering"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">loadInitialDrivers</span>() {
    String drivers;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// 步骤2先读取</span>
        drivers = AccessController.doPrivileged(<span class="hljs-keyword">new</span> PrivilegedAction<String>() {
            <span class="hljs-keyword">public</span> String <span class="hljs-title">run</span>() {
                <span class="hljs-keyword">return</span> System.getProperty(<span class="hljs-string">"jdbc.drivers"</span>);
            }
        });
    } <span class="hljs-keyword">catch</span> (Exception ex) {
        drivers = <span class="hljs-keyword">null</span>;
    }
    <span class="hljs-comment">// 步骤1,通过 Service Provider Interface 获取</span>
    <span class="hljs-comment">// 感兴趣的可以看ServiceLoader.load(Driver.class)的源码</span>
    AccessController.doPrivileged(<span class="hljs-keyword">new</span> PrivilegedAction<Void>() {
        <span class="hljs-keyword">public</span> Void <span class="hljs-title">run</span>() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            <span class="hljs-keyword">try</span>{
                <span class="hljs-keyword">while</span>(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } <span class="hljs-keyword">catch</span>(Throwable t) {
                <span class="hljs-comment">// Do nothing</span>
            }
            <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
        }
    });
    <span class="hljs-keyword">if</span> (drivers == <span class="hljs-keyword">null</span> || drivers.equals(<span class="hljs-string">""</span>)) {
        <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-comment">// 步骤1失败后进行使用步骤2之前读取的内容</span>
    String[] driversList = drivers.split(<span class="hljs-string">":"</span>);
    println(<span class="hljs-string">"number of Drivers:"</span> + driversList.length);
    <span class="hljs-keyword">for</span> (String aDriver : driversList) {
        <span class="hljs-keyword">try</span> {
            println(<span class="hljs-string">"DriverManager.Initialize: loading "</span> + aDriver);
            <span class="hljs-comment">// 直接获取系统类加载器,不适用于多ClassLoader的java web环境</span>
            Class.forName(aDriver, <span class="hljs-keyword">true</span>,
                    ClassLoader.getSystemClassLoader());
        } <span class="hljs-keyword">catch</span> (Exception ex) {
            println(<span class="hljs-string">"DriverManager.Initialize: load failed: "</span> + ex);
        }
    }
}</code>

代码样例

以mysql为例,观察步骤3的使用方式。先看一下用户注册驱动及获取connection的过程:

<code class="language-java hljs  has-numbering"><span class="hljs-comment">// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类</span>
Class.forName(<span class="hljs-string">"com.mysql.jdbc.Driver"</span>).newInstance(); 
String url = <span class="hljs-string">"jdbc:mysql://localhost:3306/testdb"</span>;    
<span class="hljs-comment">// 通过java库获取数据库连接</span>
Connection conn = java.sql.DriverManager.getConnection(url, <span class="hljs-string">"name"</span>, <span class="hljs-string">"password"</span>); </code>

源码解读

Class.forName()com.mysql.jdbc.Driver.Class加载到了AppClassLoader,注意该类是java.sql.Driver接口的实现(class Driver implements java.sql.Driver),它们类名相同。

com.mysql.jdbc.Driver在加载后将运行其静态代码块:

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">static</span> {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// 这儿driver实例依靠的是AppClassLoader中的class实例化的</span>
        java.sql.DriverManager.registerDriver(<span class="hljs-keyword">new</span> com.mysql.jdbc.Driver());
    } <span class="hljs-keyword">catch</span> (SQLException E) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> RuntimeException(<span class="hljs-string">"Can't register driver!"</span>);
    }
}</code>

registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。

好,接下来的java.sql.DriverManager.getConnection()才算是进入了正戏。它最终调用了以下方法:

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> Connection <span class="hljs-title">getConnection</span>(
     String url, java.util.Properties info, Class<?> caller) <span class="hljs-keyword">throws</span> SQLException {
     <span class="hljs-comment">/* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿调用者是java.sql.DriverManager(位于/lib/rt.jar中),
      * 也就是说caller.getClassLoader()本应得到Bootstrap启动类加载器
      * 但是在上一篇文章中讲到过启动类加载器无法被程序获取,所以只会得到null
      */</span>
     ClassLoader callerCL = caller != <span class="hljs-keyword">null</span> ? caller.getClassLoader() : <span class="hljs-keyword">null</span>;
     <span class="hljs-keyword">synchronized</span>(DriverManager.class) {
         <span class="hljs-comment">// 获取线程上下文类加载器,用于后续校验</span>
         <span class="hljs-keyword">if</span> (callerCL == <span class="hljs-keyword">null</span>) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }

     <span class="hljs-keyword">if</span>(url == <span class="hljs-keyword">null</span>) {
         <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> SQLException(<span class="hljs-string">"The url cannot be null"</span>, <span class="hljs-string">"08001"</span>);
     }

     SQLException reason = <span class="hljs-keyword">null</span>;
     <span class="hljs-comment">// 遍历注册到registeredDrivers里的Driver类</span>
     <span class="hljs-keyword">for</span>(DriverInfo aDriver : registeredDrivers) {
         <span class="hljs-comment">// 使用线程上下文类加载器检查Driver类有效性,重点在isDriverAllowed中,方法内容在后面</span>
         <span class="hljs-keyword">if</span>(isDriverAllowed(aDriver.driver, callerCL)) {
             <span class="hljs-keyword">try</span> {
                 println(<span class="hljs-string">"    trying "</span> + aDriver.driver.getClass().getName());
                 <span class="hljs-comment">// 调用com.mysql.jdbc.Driver.connect方法获取连接</span>
                 Connection con = aDriver.driver.connect(url, info);
                 <span class="hljs-keyword">if</span> (con != <span class="hljs-keyword">null</span>) {
                     <span class="hljs-comment">// Success!</span>
                     <span class="hljs-keyword">return</span> (con);
                 }
             } <span class="hljs-keyword">catch</span> (SQLException ex) {
                 <span class="hljs-keyword">if</span> (reason == <span class="hljs-keyword">null</span>) {
                     reason = ex;
                 }
             }

         } <span class="hljs-keyword">else</span> {
             println(<span class="hljs-string">"    skipping: "</span> + aDriver.getClass().getName());
         }

     }
     <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> SQLException(<span class="hljs-string">"No suitable driver found for "</span>+ url, <span class="hljs-string">"08001"</span>);
 }</code>
<code class="hljs java has-numbering"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isDriverAllowed</span>(Driver driver, ClassLoader classLoader) {
    <span class="hljs-keyword">boolean</span> result = <span class="hljs-keyword">false</span>;
    <span class="hljs-keyword">if</span>(driver != <span class="hljs-keyword">null</span>) {
        Class<?> aClass = <span class="hljs-keyword">null</span>;
        <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// 传入的classLoader为调用getConnetction的线程上下文类加载器,从中寻找driver的class对象</span>
            aClass =  Class.forName(driver.getClass().getName(), <span class="hljs-keyword">true</span>, classLoader);
        } <span class="hljs-keyword">catch</span> (Exception ex) {
            result = <span class="hljs-keyword">false</span>;
        }
    <span class="hljs-comment">// 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个</span>
    <span class="hljs-comment">// driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader</span>
        result = ( aClass == driver.getClass() ) ? <span class="hljs-keyword">true</span> : <span class="hljs-keyword">false</span>;
    }

    <span class="hljs-keyword">return</span> result;
}</code>

其中线程上下文类加载器的作用主要用于校验存放的driver是否和调用时的一致。

当然我们也可以直接调用子类的com.mysql.jdbc.Driver().connect(...)DriverManager.getConnection()最终就是调用该方法的)来得到数据库连接,也就是说我自己创建个public staic的 Driver,然后在需要的地方都调用这个类来获取Connection,完全不用系统的DriverManager。但自然是不推荐这种做法了,一来这么写没有什么设计模式的思想,二来失去了统一注册的优势,比如第三方jar包只能通过DriverManager获取Connection,而你又没法修改它的源码。

Tomcat与spring的类加载器案例

接下来将介绍《深入理解java虚拟机》一书中的案例,并解答它所提出的问题。(部分类容来自于书中原文)

Tomcat中的类加载器

在Tomcat目录结构中,有三组目录(“/common/”,“/server/”和“shared/”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/”,把java类库放置在这些目录中的含义分别是:

  • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
  • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示
Tomcat中的类加载器

灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/、/server/、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

Spring加载问题

Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

解答

答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用线程上下文加载器来加载类,而线程上下文加载器默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,简直完美~

源码分析

有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

<code class="language-java hljs  has-numbering"><span class="hljs-keyword">public</span> WebApplicationContext <span class="hljs-title">initWebApplicationContext</span>(ServletContext servletContext) {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// 创建WebApplicationContext</span>
        <span class="hljs-keyword">if</span> (<span class="hljs-keyword">this</span>.context == <span class="hljs-keyword">null</span>) {
            <span class="hljs-keyword">this</span>.context = createWebApplicationContext(servletContext);
        }
        <span class="hljs-comment">// 将其保存到该webapp的servletContext中     </span>
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, <span class="hljs-keyword">this</span>.context);
        <span class="hljs-comment">// 获取线程上下文类加载器,默认为WebAppClassLoader</span>
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        <span class="hljs-comment">// 如果spring的jar包放在每个webapp自己的目录中</span>
        <span class="hljs-comment">// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader</span>
        <span class="hljs-keyword">if</span> (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = <span class="hljs-keyword">this</span>.context;
        }
        <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (ccl != <span class="hljs-keyword">null</span>) {
            <span class="hljs-comment">// 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来</span>
            <span class="hljs-comment">// 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出</span>
            currentContextPerThread.put(ccl, <span class="hljs-keyword">this</span>.context);
        }

        <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>.context;
    }
    <span class="hljs-keyword">catch</span> (RuntimeException ex) {
        logger.error(<span class="hljs-string">"Context initialization failed"</span>, ex);
        <span class="hljs-keyword">throw</span> ex;
    }
    <span class="hljs-keyword">catch</span> (Error err) {
        logger.error(<span class="hljs-string">"Context initialization failed"</span>, err);
        <span class="hljs-keyword">throw</span> err;
    }
}</code>

具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用线程上下文类加载器来解决所有可能面临的情况。

总结

通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:
1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

简而言之就是ContextClassLoader默认存放了AppClassLoader的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。


天天编码 , 版权所有丨本文标题:真正理解线程上下文类加载器
转载请保留页面地址:http://www.tiantianbianma.com/thread-context-classloader.html/
喜欢 (1)
支付宝[多谢打赏]
分享 (0)
发表我的评论
取消评论

表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址