JVM 线程上下文类加载器

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

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 相关的代码可能还需要去调用具体实现类中的代码,但是它是无法看到具体的实现类的(因为是由其子加载器加载的)。

SPI机制是JDK提供接口,第三方Jar包实现,接口由启动类加载器加载,实现类不在JDK中,需要反向委派,由线程上下文加载器加载

而这个问题,不仅是在 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 的类加载器是首先尝试自己加载,自己加载不了才委托给它的双亲,这于传统的双亲委托模型是相反的。

SPI(Service Provider Interface)

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

我们在使用 JDBC 时,都需要加载 Driver 驱动,不知道你注意到没有,不写

Class.forName("com.mysql.jdbc.Driver")

也是可以让 com.mysql.jdbc.Driver 正确加载的,你知道是怎么做的吗?
让我们追踪一下源码:

public class DriverManager {
    // 注册驱动的集合
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers
        = new CopyOnWriteArrayList<>();
    // 初始化驱动
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

先不看别的,看看 DriverManager 的类加载器:

System.out.println(DriverManager.class.getClassLoader());

打印 null,表示它的类加载器是 Bootstrap ClassLoader,会到 JAVA_HOME/jre/lib 下搜索类,但JAVA_HOME/jre/lib 下显然没有 mysql-connector-java-5.1.47.jar 包,这样问题来了,在DriverManager 的静态代码块中,怎么能正确加载 com.mysql.jdbc.Driver 呢?

继续看 loadInitialDrivers() 方法:

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;
    }
    // 1)使用 ServiceLoader 机制加载驱动,即 SPI
    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;
        }
    });
    println("DriverManager.initialize: jdbc.drivers = " + drivers);
    // 2)使用 jdbc.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);
            // 这里的 ClassLoader.getSystemClassLoader() 就是应用程序类加载器
                Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

先看 2)发现它最后是使用 Class.forName 完成类的加载和初始化( Class.forName(aDriver, true, ClassLoader.getSystemClassLoader());),关联的是应用程序类加载器,因此可以顺利完成类加载
再看 1)它就是大名鼎鼎的 Service Provider Interface (SPI)约定如下,在 jar 包的 META-INF/services 包下,以接口全限定名名为文件,文件内容是实现类名称

在这里插入图片描述

这样就可以使用

ServiceLoader<接口类型> allImpls = ServiceLoader.load(接口类型.class);
Iterator<接口类型> iter = allImpls.iterator();
while(iter.hasNext()) {
    iter.next();
}

来得到实现类,体现的是【面向接口编程+解耦】的思想,在下面一些框架中都运用了此思想:

  • JDBC
  • Servlet 初始化器
  • Spring 容器
  • Dubbo(对 SPI 进行了扩展)

接着看 ServiceLoader.load 方法:

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取线程上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器(JVM会默认把应用程序类加载器赋值给当前线程),它内部又是由Class.forName 调用了线程上下文类加载器完成类加载,具体代码在 ServiceLoader 的内部类

LazyIterator 中:3

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,"Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,
             "Provider " + cn
             + " not a subtype");
    }
    try {
        S p = service.cast(c.newInstance());
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();
    // This cannot happen
}
  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值