Java中的上下文加载器ContextClassLoader详解

 更新时间:2023年10月07日 09:46:43   作者:邋遢的流浪剑客  
这篇文章主要介绍了Java中的上下文加载器ContextClassLoader详解,ContextClassLoader是通过Thread.currentThread().getContextClassLoader()返回该线程上下文的ClassLoader,需要的朋友可以参考下

ContextClassLoader

ContextClassLoader是通过 Thread.currentThread().getContextClassLoader() 返回该线程上下文的ClassLoader

1、前置知识

在讲解ContextClassLoader之前,需要先提两个知识点:

1)双亲委派模型

在这里插入图片描述

  • 启动类加载器(Bootstrap ClassLoader):负责将放在<JAVA HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
  • 扩展类加载器(ExtClassLoader):由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
  • 应用程序类加载器(AppClassLoader):由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载。它负责加载用户类路径(ClassPath)上所有指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器

类加载之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码

双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object ,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类

2)如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载

比如Spring作为一个Bean工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring是通过调用 Class.forName 来加载业务类的。调用 Class.forName() 的时候,会获取调用该方法的类的类加载器,使用该类加载器来加载 Class.forName() 中传入的类,代码如下:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    @CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
      	// 获取调用该方法的类
        Class<?> caller = Reflection.getCallerClass();
      	// ClassLoader.getClassLoader获取调用该方法的类的类加载器
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

2、为什么需要ContextClassLoader?

当我们需要加载一个类,从自定义ClassLoader,到AppClassLoader,再到ExtClassLoader,最后到Bootstrap ClassLoader。没问题, 很顺利。这是从下到上加载。但是反过来,当从上到下加载的时候,这个变得是一个不可能完成的任务。为了弥补这个缺陷, 特定设计的ContextClassLoader

这里你可能会有个疑问:为什么会出现从上到下加载的情况。比如一个类是由Bootstrap ClassLoader加载,该类引用了一个我们自己开发的类(该类能被AppClassLoader加载但不能被Bootstrap ClassLoader加载),由如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载可知:默认情况下我们自己开发的类会被Bootstrap ClassLoader尝试加载,最终会由于无法加载到类而抛出异常

以SPI为例,SPI接口属于Java核心库,由BootstrapClassLoader加载,当SPI接口想要引用第三方实现类的具体方法时,BootstrapClassLoader无法加载Classpath下的第三方实现类,这时就需要使用线程上下文类加载器ContextClassLoader来解决。借助这种机制可以打破双亲委托机制限制

SPI核心类ServiceLoader源码如下:

public final class ServiceLoader<S>
    implements Iterable<S>
{
    public static <S> ServiceLoader<S> load(Class<S> service) {
      	// 线程上下文类加载器,在Launcher类的构造器中被赋值为AppClassLoader,它可以读到ClassPath下的自定义类
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }     

在这里插入图片描述

3、ContextClassLoader默认为AppClassLoader

JVM启动时,会去调用Launcher类的构造方法:

public class Launcher {
    public Launcher() {
        ClassLoader extcl;
        try {
            // 首先创建扩展类加载器
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                    "Could not create extension class loader");
        }
        // Now create the class loader to use to launch the application
        try {
            // 再创建AppClassLoader并把extcl作为父加载器传递给AppClassLoader
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                    "Could not create application class loader");
        }
        // 设置线程上下文类加载器,稍后分析
        Thread.currentThread().setContextClassLoader(loader);
        // 省略其他代码...
    }

Launcher初始化时首先会创建ExtClassLoader类加载器,然后再创建AppClassLoader并把ExtClassLoader传递给它作为父类加载器,还把AppClassLoader默认设置为线程上下文类加载器

4、子线程ContextClassLoader默认为父线程的ContextClassLoader

Thread在 init() 方法中会把子线程ContextClassLoader设置为父线程的ContextClassLoader

public
class Thread implements Runnable {
    private ClassLoader contextClassLoader;
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        // 省略其他代码...
      	// 当前线程为父线程
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */
            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }
            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();
        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
        g.addUnstarted();
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
      	// 子线程ContextClassLoader设置为父线程的ContextClassLoader
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        // 省略其他代码...
    }

到此这篇关于Java中的上下文加载器ContextClassLoader详解的文章就介绍到这了,更多相关ContextClassLoader详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 解决MyBatis @param注解参数类型错误异常的问题

    解决MyBatis @param注解参数类型错误异常的问题

    这篇文章主要介绍了解决MyBatis @param注解参数类型错误异常的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • SpringBoot2.1.4中的错误处理机制

    SpringBoot2.1.4中的错误处理机制

    这篇文章主要介绍了SpringBoot2.1.4中的错误处理机制,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • spring boot 如何请求后缀匹配

    spring boot 如何请求后缀匹配

    这篇文章主要介绍了spring boot 请求后缀匹配的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-09-09
  • Java调用CMD命令的方法与使用技巧

    Java调用CMD命令的方法与使用技巧

    在实际的开发中我们有可能会遇到 java调用 cmd命令的情况,这篇文章主要给大家介绍了关于Java调用CMD命令的方法与使用的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-09-09
  • Java中while循环的使用方法举例详解

    Java中while循环的使用方法举例详解

    在Java编程语言中,while循环是基础控制结构之一,用于重复执行代码块直至满足特定条件,掌握其使用是编程的基础,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-09-09
  • Java多线程CountDownLatch的实现

    Java多线程CountDownLatch的实现

    本文主要介绍了Java多线程CountDownLatch的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-02-02
  • Spring Bean生命周期源码原理图解

    Spring Bean生命周期源码原理图解

    这篇文章主要介绍了Spring Bean生命周期源码原理图解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • Java使用注解实现防止重复提交实例

    Java使用注解实现防止重复提交实例

    这篇文章主要介绍了Java使用注解实现防止重复提交实例,在一些项目中由于用户误操作,多次点击表单提交按钮,会产生很多次的数据交互,为了解决这一问题,本文使用注解来实现防止重复提交,需要的朋友可以参考下
    2023-07-07
  • Java中的ProcessBuilder类详细解析

    Java中的ProcessBuilder类详细解析

    这篇文章主要介绍了Java中的ProcessBuilder类详细解析,ProcessBuilder类是J2SE 1.5在java.lang中新添加的一个新类,此类用于创建操作系统进程,它提供一种启动和管理进程的方法,需要的朋友可以参考下
    2024-01-01
  • java中synchronized(同步代码块和同步方法)详解及区别

    java中synchronized(同步代码块和同步方法)详解及区别

    这篇文章主要介绍了 java中synchronized(同步代码块和同步方法)详解及区别的相关资料,需要的朋友可以参考下
    2017-02-02

最新评论