美文网首页
深入探索“线程上下文类加载器”

深入探索“线程上下文类加载器”

作者: tomas家的小拨浪鼓 | 来源:发表于2019-08-04 00:27 被阅读0次

    内容概述

    • “线程上下文类加载器”介绍
    • SPI(Service Provider Interface)探索
    • 通过JDBC驱动加载深刻理解线程上下文类加载器机制
      < br>

    “线程上下文类加载器”介绍

    线程上下问类加载器出现的原因

    Q: 越基础的类由越上层的加载器进行加载,如果基础类又要调用回用户的代码,那该怎么办?
    A: 解决方案:使用“线程上下文类加载器”

    为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

    有了线程上下文类加载器,也就是父类加载器请求子类加载器去完成类加载的动作(即,父类加载器加载的类,使用线程上下文加载器去加载其无法加载的类),这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则。
    Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

    JDBC 使用伪代码:

    Class.forName("com.mysql.driver.Driver");
    Connection conn = Driver.getConnection();
    Statement st = conn.getStatement();
    

    JDBC 是一个标准。不同的数据库厂商(如,mysql、oracle等)会根据这个标准,有它们自己的实现。
    既然,JDBC 是一个标准,那么 JDBC 的接口,应该就已经存在与了 JDK 中了。(JDBC 相关的接口存在与 rt.jar 的java.sql 包下)

    因此,JDBC 相关的这些接口,在启动的时候,是由启动类加载器(boost classLoader)去加载的。
    而通常,我们会将数据库厂商提供的 jar 包放置在 classPath 下,由此可知,数据库厂商所提供的实现类不会由启动类加载器来去加载,它们通常是由系统类加载器来去加载的。
    这样一来,接口是有启动类加载器加载的,而具体的实现是由应用类加载器加载的。根据类的双亲委托原则,父加载器所加载的类/接口是看不到子加载器所加载的类/接口的,而然,子加载器所加载的类/接口是能够看到父加载器的类/接口的。这样的话,会导致这样一个局面:JDBC 相关的代码可能还需要去调用具体实现类中的代码,但是它是无法看到具体的实现类的(因为是由其子加载器加载的)。
    而这个问题,不仅是在 JDBC 中出现,在 JNDI、xml解析,等场景下都会出现。
    总结来说,在 SPI 这种场合下都会出现的问题。

    Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。

    当前类加载器(Current Classloader)

    每个类都会使用自己的类加载器(即,加载自身的类加载器)来去加载其他的类(指的是所依赖的类),如果 ClassX 引用了 ClassY,那么 ClassX 的类加载器就会去加载 ClassY(前提是 ClassY 尚未被加载)

    线程上下文类加载器(Context Classloader)

    线程上下文类加载器是从 JDK1.2 开始引入的,类 Thread 中的 getContextClassLoader() 与 setContextClassLoader(ClassLoader cl) 分别用来获取和设置上下文类加载器。
    如果没有通过 setContextClassLoader(ClassLoader cl) 进行设置的话,线程将继承其父线程的上下文类加载器。
    Java 应用运行时的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。

    线程上下文类加载器的重要性:

    SPI (Service Provider Interface ———— 服务提供者接口)
    父ClassLoader 可以使用当前线程 Thread.currentThread().getContextClassLoader() 所指定的 classloader 加载的类。
    这就改变了 父ClassLoader 不能使用 子ClassLoader 或是其他没有直接父子关系的 ClassLoader 加载的类的情况,即,改变了双亲委托模型。
    线程上下文类加载器就是当前线程的 Current ClassLoader。
    在双亲委托模型下,类加载器是由下至上的,即下层的类加载器会委托上层进行加载。但是对于 SPI 来说,有些接口是 Java 核心库所提供的,而 Java 核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的 jar 包(厂商提供),Java 的启动类加载器是不会加载其他来源的 jar 包,这样传统的双亲委托模型就无法满足 SPI 的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

    在框架开发、底层组件开发、应用服务器、web服务器的开发,就会用到线程上下文类加载器。比如,tomcat 框架,就对加载器就做了比较大的改造。
    tomcat 的类加载器是首先尝试自己加载,自己加载不了才委托给它的双亲,这于传统的双亲委托模型是相反的。

    线程上下文类加载器的一般模式

    线程上下文类加载器的一般使用模式(这个在框架中是大量应用的):获取 -> 使用 -> 还原

    # 伪代码:
        // 获取
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try {
    
            // 使用
            Thread.currentThread().setContextClassLoader(targetTccl);
            myMethod();
    
        } finally {
    
            // 还原
            Thread.currentThread().setContextClassLoader(classLoader);
        }
    

    myMethod 里面则调用了 Thread.currentThread().getContextClassLoader(),获取当前线程的上下文类加载器做某些事情。

    如果一个类由类加载器A加载,那么这个类的依赖也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话)

    ContextClassLoader 的作用就是为了破坏 Java 的类加载委托机制。

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

    如果我们没有对线程上下文类加载器做任何设值的话,那么当前线程的上下文类加载器就是"系统类加载器"。

    Q:其实只要是能加载目标类的任何加载器都可以实现打破委托模式的目的,那么为什么一定通过线程上下文类加载来实现了?
    A:因为任何的 Java 代码都是运行某个线程上的,因此将这个打破委托模式的类加载器放在线程中是最合适的。

    SPI(Service Provider Interface)探索

    SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。

    通过 MySql 来讲解 SPI
    1. 添加 MySql 依赖
    # 修改 gradle 文件
    dependencies {
    //    testCompile group: 'junit', name: 'junit', version: '4.12'
    
        compile (
                "mysql:mysql-connector-java:5.1.34"
        )
    }
    
    1. ServiceLoader
      很多 SPI 的实现都是通过 ServiceLoader 来去完成的。
    # MyTest26
    public class MyTest26 {
    
        public static void main(String[] args) {
    
            // 很多 SPI 的实现都是通过 ServiceLoader 来去完成的。
            ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
    
            // JDK 的类加载器所能寻找到的所有驱动
            Iterator<Driver> iterator = loader.iterator();
    
            while (iterator.hasNext()) {
                Driver driver = iterator.next();
    
                System.out.println("driver : " + driver.getClass() + " , loader : " + driver.getClass().getClassLoader());
            }
    
            System.out.println("当前线程上线文类加载器 : " + Thread.currentThread().getContextClassLoader());
            System.out.println("ServiceLoader的类加载器 : " + ServiceLoader.class.getClassLoader());
    
        }
    }
    
    # 控制台
    driver : class com.mysql.jdbc.Driver , loader : sun.misc.Launcher$AppClassLoader@18b4aac2
    driver : class com.mysql.fabric.jdbc.FabricMySQLDriver , loader : sun.misc.Launcher$AppClassLoader@18b4aac2
    当前线程上线文类加载器 : sun.misc.Launcher$AppClassLoader@18b4aac2
    ServiceLoader的类加载器 : null
    

    👆总共找到了 2 个驱动,都是从我们引入 mysql jar 包中找到的。
    『driver : class com.mysql.jdbc.Driver , loader : sun.misc.Launcher$AppClassLoader@18b4aac2』:这个”com.mysql.jdbc.Driver"就是我们刚刚应用进来的 mysql 包里的类,也就是说,它的包在 classPath 中。

    注意,从👆的结果我们能发现。ServiceLoader 是由“启动类加载器”加载的,而它确认能够获取到“系统类加载器”加载的’com.mysql.jdbc.Driver’和’com.mysql.fabric.jdbc.FabricMySQLDriver’。
    而且,ServiceLoader 又是根据什么机制,如何找到的这两个驱动了??

    那么,接下来我们来深入来接下 ServiceLoader 类

    ServiceLoader

    一个简单的加载服务器提供者的设施。(即,用于加载服务器提供者,也就是服务的具体实现)

    一个服务(service)是一个已知的接口和类(通常是抽象类)。 一个服务器提供者(service provider)就是服务(service)的一个具体实现。一个服务提供者类通常都会实现服务接口或子类化服务本身所定义的类。 服务提供者能通过扩展的方式安装进 Java 平台,比如,以jar文件的形式放置到任何常规的扩展目录下。而提供者还可以通过将其加到应用的 classPath 或者特定于平台的方式使其(即,jar包)变得可用。

    处于加载的目标,一个服务是由单个的类型来表示的,也就是说,一个单个的接口或者抽象类(一个具体的类也能够被使用,但这是不推荐的)。一个给定服务的提供者它会包含一个或多个具体的类,这些类通过数据和提供者特有的代码来扩展服务类型。 提供者类通常不是提供者本身,而是一个代理,它包含足够的信息可以确定提供者是否可用满足特定的一个需求并根据需要创建真实的提供者。提供者的细节与特定的服务类型高度相关,没有单个的类或接口能够将它们(即,所有的服务类型)统一起来,因此没有这样的一个类型被定义在此。这个设置(ServiceLoader)的唯一强制性需求是:提供者类必须要有一个无参数的构造函数,这样它们可以在加载的时候就被实例化。

    服务提供者通过将一个‘provider-configuration’文件放置到资源目录的『META-INF/services』下,而且文件的名字是服务类型的一个全限定的二进制名。这个文件会包含具体提供者类的完全限定的二进制名字的列表,并每行放置一个。空格、tab键、以及空行都会被忽略掉。注释字符是『#』;位于第一个注释的后的所有字符都被被忽略(即,注释内容会被忽略)。并且该文件必须是 UTF-8 编码。
    👆这段说的是,如果提供者是如果告诉JDK我所提供的具体类是什么的。就是通过‘provider-configuration’(文件的真实名字是服务类型的一个全限定的二进制名)配置文件来告知的,以及配置文件的格式是什么。

    # ServiceLoader 中的静态常量
    private static final String PREFIX = "META-INF/services/";
    

    如果一个具体的提供者类出现在了多个配置文件中,或者一个具体提供者类的名字在一个文件中出现了多次,那么重复的就会被忽略掉。配置文件的名字是不需要位于相同的 jar 包中或者其他的分发单元里。提供者必须能够通过相同的类加载器(即,最初去定位这个配置文件的类加载器)来去加载出来的;注意,但并不要求是真实加载文件的类加载器(👈,即要求的是’初始加载器’,也就是请求加载类的加载器,并不要求是’定义类加载器’,也就是真实加载类的加载器)。

    • 通过查看 Mysql jar 包的 META-INF/services 目录,以及 MyTest26 的程序结果。来验证上面的理论


    # java.sql.Driver 文件 ——— 文件的名字是服务类型的一个全限定的二进制名
    com.mysql.jdbc.Driver
    com.mysql.fabric.jdbc.FabricMySQLDriver
    

    提供者是延迟定位和实例化的,也就是按需来去定位和实例化的。一个服务加载器会维护着一个到目前为止已经被加载的提供者的缓存列表。而,每一次对于 iterator 方法的调用都会返回一个迭代器,该迭代器首先会获取缓存中所有的元素,并且获取的顺序为元素实例化的顺序。然后,延迟定位和实例化剩余的提供者,并且将其按照顺序添加到缓冲中。缓冲可以通过 reload 方法进行清空。(代码见👇)

    # ServiceLoader 中的成员属性
    // Cached providers, in instantiation order
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    

    这个类的实例对于多个并发线程来说并不是安全的。(即,ServiceLoader 不是一个线程安全的类)

    除非有其他的指定,传入一个 null 参数给该类中的任何方法都将导致一个 NullPointerException 异常被抛出。

    示例:

    Suppose we have a service type com.example.CodecSet which is intended to represent sets of encoder/decoder pairs for some protocol. In this case it is an abstract class with two abstract methods:
       public abstract Encoder getEncoder(String encodingName);
       public abstract Decoder getDecoder(String encodingName);
    Each method returns an appropriate object or null if the provider does not support the given encoding. Typical providers support more than one encoding.
    If com.example.impl.StandardCodecs is an implementation of the CodecSet service then its jar file also contains a file named
       META-INF/services/com.example.CodecSet
    This file contains the single line:
       com.example.impl.StandardCodecs    # Standard codecs
    The CodecSet class creates and saves a single service instance at initialization:(CodecSet 类会保存一个单个服务的实例在初始化的时候)
       private static ServiceLoader<CodecSet> codecSetLoader
           = ServiceLoader.load(CodecSet.class);
    To locate an encoder for a given encoding name it defines a static factory method which iterates through the known and available providers, returning only when it has located a suitable encoder or has run out of providers.
       public static Encoder getEncoder(String encodingName) {
           for (CodecSet cp : codecSetLoader) {
               Encoder enc = cp.getEncoder(encodingName);
               if (enc != null)
                   return enc;
           }
           return null;
       }
    A getDecoder method is defined similarly.
    

    使用注意,如果一个用于加载提供者的 类加载器 的 classpath 包含远程的网络url,那么在搜索‘provider-configuration’文件的时候这些url将会被解引用。

    ServiceLoader中的’泛型’就是,服务加载器想要去加载的服务类型。

    MyTest26 代码讲解

    ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
    主要完成了:
    ① ServiceLoader 的实例化。即,构建了一个 ServiceLoader 对象
    ② 构建了一个 LazyIterator

    1. public static <S> ServiceLoader<S> load(Class<S> service)
    • private ServiceLoader(Class<S> svc, ClassLoader cl) ———— 构造方法

    • public void reload()

    1. Iterator<Driver> iterator = loader.iterator();


    MyTest26 示例修改
    因为 扩展类加载器 是加载不到我们 MySql 的驱动的,因此 iterator.hasNext() 返回 false。

    通过JDBC驱动加载深刻理解线程上下文类加载器机制

    示例
    # MyTest27
    public class MyTest27 {
    
        public static void main(String[] args) throws Exception {
            Class.forName("com.mysql.jdbc.Driver");
            Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mytestdb", "username", "password");
        }
    }
    
    第一步

    Class.forName("com.mysql.jdbc.Driver");
    会去加载并初始化“com.mysql.jdbc.Driver”

    # com.mysql.jdbc.Driver
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        //
        // Register ourselves with the DriverManager
        //
        static {
            try {
                java.sql.DriverManager.registerDriver(new Driver());
            } catch (SQLException E) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    

    可见,初始化”com.mysql.jdbc.Driver”的过程会去:
    a)初始化“java.sql.DriverManager”(启动类加载器加载的)。它会:
    ① 加载“jdbc.properties”属性所指定的服务。
    ② 同时也会,使用 ServiceLoader 机制(即,使用线程上下文类加载器(本例中即为系统类加载器)来加载指定目录下(META-INF/services)指定服务类型的服务提供者类)来加载“java.sql.Driver” 类型的服务提供类(本例为 com.mysql.jdbc.Driver、com.mysql.fabric.jdbc.FabricMySQLDriver)。这样使得,在java.sql.DriverManager 的Class对象中就可以使用这些服务提供者类了。(所以,第二步的’getConnection’就是一个静态方法。)
    注意,这里就是启动类加载器加载的类可以使用线程上下文类加载器加载的类了。其实就是通过线程上下文类加载器,将启动类加载器不可见的类加载了进来,这样启动类加载器加载的这个类就可以去使用它们了。
    b)将 com.mysql.jdbc.Driver 的实例注册到 java.sql.DriverManager 中

    • a)初始化“java.sql.DriverManager”
    # java.sql.DriverManager
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    private static volatile int loginTimeout = 0;
    private static volatile java.io.PrintWriter logWriter = null;
    private static volatile java.io.PrintStream logStream = null;
    // Used in println() to synchronize logWriter
    private final static  Object logSync = new Object();
    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
    
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
    
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
    
                /* Load these drivers, so that they can be instantiated.
                 * It may be the case that the driver class may not be there
                 * i.e. there may be a packaged driver with the service class
                 * as implementation of java.sql.Driver but the actual class
                 * may be missing. In that case a java.util.ServiceConfigurationError
                 * will be thrown at runtime by the VM trying to locate
                 * and load the service.
                 *
                 * Adding a try catch block to catch those runtime errors
                 * if driver not available in classpath but it's
                 * packaged as service and that service is there in classpath.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
    
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
    
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
    

    初始化“java.sql.DriverManager”的过程:
    加载 java.sql.Driver 类型的服务提供类。注意,需要调用 Iterator的hasNext()和next()方法才能实现对“java.sql.Driver的服务提供类”的初始化。

    第二步

    Connection conn = DriverManager.getConnection("jdbc:[mysql://localhost:3306/mytestdb](mysql://localhost:3306/mytestdb)", "username", "password”);
    该方法底层要求,调用该方法对象的类加载器,要和执行“Class.forName("com.mysql.jdbc.Driver");”方法线程的上下文类加载器是同一个类加载器。
    也就是说,保证了加载 服务提供者类的类加载器 与 执行“DriverManager.getConnection(...);”方法的对象的类加载器是同一个类加载器。
    这是因为,“DriverManager.getConnection(…)”方法中会去使用 服务提供者类

    同时值得关注的是,借用该方式,就可以在 java.sql.DriverManager 对象(由,启动类加载器加载的)类调用到服务提供者类(由线程上下文类加载器加载的,这里即为系统类加载器)的方法了。


    最后,其实在该例子中。都可以将 第一步(即,Class.forName("com.mysql.jdbc.Driver”); ———— 加载并初始化com.mysql.jdbc.Driver) 去除。也能达到一样的效果

    这是因为 “DriverManager.getConnection(...)”方法会导致,DriverManager的加载和初始化,而在DriverManager初始化过程中,会有去加载(包括’初始化’) com.mysql.jdbc.Driver 类型的服务提供者类(com.mysql.jdbc.Driver、com.mysql.fabric.jdbc.FabricMySQLDriver)。

    相关文章

    深入浅出“类加载器”
    从 sun.misc.Launcher 类源码深入探索 ClassLoader
    ClassLoader 源码详解

    参考

    圣思园《深入理解JVM》

    相关文章

      网友评论

          本文标题:深入探索“线程上下文类加载器”

          本文链接:https://www.haomeiwen.com/subject/mmzsdctx.html