Really understand thread context class loader (multiple case analysis)

foreword

Previously, my understanding of the thread context class loader (ThreadContextLoader) was limited to the following paragraph:

Java provides many Service Provider Interfaces (SPIs) that allow third parties to provide implementations for these interfaces. Common SPIs are JDBC, JCE, JNDI, JAXP, and JBI.

The interfaces of these SPIs are provided by the Java core library, and the implementation codes of these SPIs are included in the class path (CLASSPATH) as the jar packages that Java applications depend on. Code in the SPI interface often needs to load specific implementation classes . Then the problem comes, the SPI interface is part of the Java core library and is loaded by the bootstrap class loader ; the SPI implementation class is loaded by the system class loader . The bootstrap classloader cannot find the SPI implementation class because, according to the parent delegation model, the BootstrapClassloader cannot delegate the AppClassLoader to load the class.

The thread context class loader destroys the "parent delegation model", which can abandon the parent delegation load chain mode in the execution thread, so that the program can use the class loader in reverse.

The question that has been bugging me is, how does it break the parental delegation model? How to use the class loader in reverse again? It wasn't until today that I saw the jdbc loading process. It was actually quite simple, but I didn't read the code and didn't understand it well enough.

JDBC case study

Let's first look at the definition of JDBC. JDBC (Java Data Base Connectivity) is a Java API for executing SQL statements , which can provide unified access to a variety of relational databases. It consists of a set of classes and interfaces written in the Java language . . JDBC provides a benchmark upon which to build more advanced tools and interfaces that enable database developers to write database applications.

That is to say, JDBC is a kind of SPI provided by java. The database provider to be connected must write the implementation class according to this standard.

The sequence of steps to load the Driver in the DriverManager in jdk is as follows:

  1. Through the SPI method, read the configuration of the file under META-INF/services and use the thread context class loader to load
  2. Get settings via System.getProperty("jdbc.drivers"), then load via system classloader
  3. User calls Class.forName()are loaded into the system class loader, and then registered in the DriverManager object, which can be used directly when needed, but it is necessary to check whether the registered and called class loading is the same. The module application has registered the mysql driver at the same time, and there will be confusion when fetching from the DriverManager)

Both steps 1 and 3 use the thread context class loader. The SPI method in 1 is similar to that in case 2. I will skip it here and focus on the method of using the thread context class loader in the next 3.

The code for steps 1 and 2 is in the initialization method loadInitialDrivers():

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

code sample

Take mysql as an example to observe the usage of step 3. Let's take a look at the process of user registration driver and obtaining connection:

// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 
  • 1
  • 2
  • 3
  • 4
  • 5

Source code interpretation

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

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

static {
    try {
        // 这儿driver实例依靠的是AppClassLoader中的class实例化的
        java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

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

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

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
      */
     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类有效性,重点在isDriverAllowed中,方法内容在后面
         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");
 }
  • 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
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
        // 传入的classLoader为调用getConnetction的线程上下文类加载器,从中寻找driver的class对象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
    // 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
    // driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

其中线程上下文类加载器的作用主要用于校验存放的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,具体方法如下(删去了部分不相关内容):

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;
    }
}
  • 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

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

总结

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

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

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326041439&siteId=291194637