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详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • @FeignClient path属性路径前缀带路径变量时报错的解决

    @FeignClient path属性路径前缀带路径变量时报错的解决

    这篇文章主要介绍了@FeignClient path属性路径前缀带路径变量时报错的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • springboot整合netty框架实现站内信

    springboot整合netty框架实现站内信

    Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,这篇文章主要介绍了springboot整合netty框架的方式小结,需要的朋友可以参考下
    2022-12-12
  • SpringBoot启动嵌入式Tomcat的实现步骤

    SpringBoot启动嵌入式Tomcat的实现步骤

    本文主要介绍了浅谈SpringBoot如何启动嵌入式Tomcat,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • Springboot 全局时间格式化三种方式示例详解

    Springboot 全局时间格式化三种方式示例详解

    时间格式化在项目中使用频率是非常高的,当我们的 API​ 接口返回结果,需要对其中某一个 date​ 字段属性进行特殊的格式化处理,通常会用到 SimpleDateFormat​ 工具处理,这篇文章主要介绍了3 种 Springboot 全局时间格式化方式,需要的朋友可以参考下
    2024-01-01
  • 使用Redis incr解决并发问题的操作

    使用Redis incr解决并发问题的操作

    这篇文章主要介绍了使用Redis incr解决并发问题的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-11-11
  • 深扒Java中POJO、VO、DO、DTO、PO、BO、AO、DAO的概念和区别以及如何应用

    深扒Java中POJO、VO、DO、DTO、PO、BO、AO、DAO的概念和区别以及如何应用

    po vo bo dto dao 和 pojo 是软件开发中经常使用的一些概念,用于设计和实现对象模型,下面将分别解释这些概念的含义及其在开发中的应用,这篇文章主要给大家介绍了关于Java中POJO、VO、DO、DTO、PO、BO、AO、DAO的概念和区别以及如何应用的相关资料,需要的朋友可以参考下
    2024-08-08
  • 如何使用MAVEN打JAR包(直接使用)

    如何使用MAVEN打JAR包(直接使用)

    这篇文章主要介绍了如何使用MAVEN打JAR包(直接使用),文中通过实例代码介绍了maven 使用assembly插件进行打包的方法,需要的朋友可以参考下
    2023-03-03
  • SpringBoot整合MongoDB的步骤详解

    SpringBoot整合MongoDB的步骤详解

    这篇文章主要介绍了SpringBoot整合MongoDB的步骤详解,帮助大家更好的理解和学习使用SpringBoot框架,感兴趣的朋友可以了解下
    2021-04-04
  • Maven打包JavaWeb项目的两种实现方式

    Maven打包JavaWeb项目的两种实现方式

    介绍了两种Maven打包Web项目的方式:通过Eclipse和通过命令行,Eclipse方式包括清理、打包、跳过测试、输入 Goals 等步骤,命令行方式包括进入项目目录、执行 clean 和 package 命令、跳过测试等步骤,注意事项包括确保有JDK环境、正确配置pom.xml文件和修改版本号
    2025-02-02
  • SpringBoot的启动速度优化

    SpringBoot的启动速度优化

    随着我们项目的不断迭代 Bean 的数量会大大增加,如果都在启动时进行初始化会非常耗时,本文主要介绍了SpringBoot的启动速度优化,感兴趣的可以了解一下
    2023-09-09

最新评论