Java中的上下文加载器ContextClassLoader详解
作者:邋遢的流浪剑客
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详解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!