zoukankan      html  css  js  c++  java
  • 线程上下文类加载

    真正理解线程上下文类加载器(多案例分析)
    2016-09-26 10:12:07      0个评论    来源:Stay Foolish  
    收藏   我要投稿

    前言

    此前我对线程上下文类加载器(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,要接入的数据库供应商必须按照此标准来编写实现类。

    代码样例

    mysql为例,先看一下驱动注册及获取connection的过程:

    1
    2
    3
    4
    5
    // 注册驱动类
    Class.forName("com.mysql.jdbc.Driver").getInstance();
    String url = "jdbc:mysql://localhost:3306/testdb";   
    // 通过java库获取数据库连接
    Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");

    Class.forName()加载了com.mysql.jdbc.Driver类,注意该类是java.sql.Driver接口的实现(class Driver extends NonRegisteringDriver implements java.sql.Driver),它们名字相同,在下面的描述中将带上package名避免混淆。

    它将运行其static静态代码块:

    1
    2
    3
    4
    5
    6
    7
    <code><code><code>static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }</code></code></code>

    registerDriver方法将本类(new com.mysql.jdbc.Driver())注册到系统的DriverManager中,其实就是add到它的成员常量中,即一个名为registeredDrivers的CopyOnWriteArrayList 。

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

    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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    <code><code><code><code><code><code><code>private static Connection getConnection(
         String url, java.util.Properties info, Class<!--?--> caller) throws SQLException {
         /* 传入的caller由Reflection.getCallerClass()得到,该方法
          * 可获取到调用本方法的Class类,这儿调用者是java.sql.DriverManager(位于/lib/rt.jar中),
          * 也就是说caller.getClassLoader()本应得到Bootstrap启动类加载器
          * 但是在上一篇文章中讲到过启动类加载器无法被程序获取,所以只会得到null
          * 这时问题来了,DriverManager是启动类加载器加载的,可偏偏又要在这儿加载子类的Class
          * 子类是通过jar包的方式放入classpath中的,由AppClassLoader加载
          * 因此这儿通过双亲委派方式肯定无法加载成功,因此这儿借助
          * ContextClassLoader来加载mysql驱动类(简直作弊啊!)
          * 上一篇文章最后也讲到了Thread.currentThread().getContextClassLoader()
          * 默认set了AppClassLoader,也就是说把类加载器放到Thread里,那么执行方法时任何地方都可以获取到它。
          */
         ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
         synchronized(DriverManager.class) {
             // 在获取线程上下文类加载器时需要同步加锁
             if (callerCL == null) {
                 callerCL = Thread.currentThread().getContextClassLoader();
             }
         }
     
         if(url == null) {
             throw new SQLException("The url cannot be null", "08001");
         }
     
         SQLException reason = null;
         // 遍历刚才放到registeredDrivers里的Driver类
         for(DriverInfo aDriver : registeredDrivers) {
             // 检查能否加载Driver类,如果你没有修改ContextClassLoader,那么默认的AppClassLoader肯定可以加载
             if(isDriverAllowed(aDriver.driver, callerCL)) {
                 try {
                     println("    trying " + aDriver.driver.getClass().getName());
                     // 调用com.mysql.jdbc.Driver.connect方法获取连接
                     Connection con = aDriver.driver.connect(url, info);
                     if (con != null) {
                         // Success!
                         return (con);
                     }
                 } catch (SQLException ex) {
                     if (reason == null) {
                         reason = ex;
                     }
                 }
     
             } else {
                 println("    skipping: " + aDriver.getClass().getName());
             }
     
         }
         throw new SQLException("No suitable driver found for "+ url, "08001");
     }</code></code></code></code></code></code></code>

    其中线程上下文类加载器的作用已经在上面的注释中详细说明了,由于SPI提供了接口,其中用connect()方法获取连接,数据库厂商必须实现该方法,然而调用时却是通过SPI里的DriverManager来加载外部实现类并调用com.mysql.jdbc.Driver.connect()来获取connection,所以这儿只能拜托Thread中保存的AppClassLoader来加载了,完全破坏了双亲委派模式。

    当然我们也可以不用SPI接口,直接调用子类的com.mysql.jdbc.Driver().connect(...)来得到数据库连接,但不推荐这么做(DriverManager.getConnection()最终就是调用该方法的)。

    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 如何访问并不在其加载范围的用户程序呢?

    解答

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

    源码分析

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

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

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

    总结

    通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

    1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

    2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

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

  • 相关阅读:
    51nod 1179 最大的最大公约数 (数论)
    POJ 3685 二分套二分
    POJ 3045 贪心
    LIC
    HDU 1029 Ignatius and the Princess IV
    HDU 1024 Max Sum Plus Plus
    HDU 2389 Rain on your Parade
    HDU 2819 Swap
    HDU 1281 棋盘游戏
    HDU 1083 Courses
  • 原文地址:https://www.cnblogs.com/tianyublog/p/8241097.html
Copyright © 2011-2022 走看看