Java的ThreadContext类加载器的实现
疑惑
以前在看源码的时候,总是会遇到框架里的代码使用Thread.currentThread.getContextClassLoader()获取当前线程的Context类加载器,通过这个Context类加载器去加载类。
我们平时在程序中写代码的时候,遇到要动态加载类的时候,一般使用Class.forName()的方式加载我们需要的类。比如最常见的,当我们进行JDBC编程的时候,我们通过Class.forName()去加载JDBC的驱动。
try {
return Class.forName("oracle.jdbc.driver.OracleDriver");
} catch (ClassNotFoundException e) {
// skip
}
那么为什么当我们使用Class.forName()的方式去加载类的时候,如果类找不到,我们还要尝试用Thread.currentThread.getContextLoader()获取的类加载器去加载类呢?比如我们可能会碰到下面这种代码:
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
// skip
} ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader();
if (ctxClassLoader != null) {
try {
clazz = ctxClassLoader.loadClass(className);
} catch (ClassNotFoundException e) {
// skip
}
}
这里加粗的部分,就是使用了Thread.currentThread.getContextLoader()获取的加载器去加载类。显然,Class.forName()加载类的时候使用的类加载器可能和Thread.currentThread.getContextLoader()获取的类加载器是不同的。那么为什么会出现不同呢?
JAVA的类加载器
要理解为什么会用到Thread.currentThread.getContextLoader()获取的这个类加载器之前,我们先来了解下JVM里使用的类加载器(ClassLoader)。
JVM默认有三种类加载器:
- Bootstrap Class Loader
- Extension Class Loader
- System Class Loader
Bootstrap Class Loader
Bootstrap Class Loader类加载器是JDK自带的一款类加载器,用于加载JDK内部的类。Bootstrap类加载器用于加载JDK中$JAVA_HOME/jre/lib下面的那些类,比如rt.jar包里面的类。Bootstrap类加载器是JVM的一部分,一般采用native代码编写。
Extension Class Loader
Extension Class Loader类加载器主要用于加载JDK扩展包里的类。一般$JAVA_HOME/lib/ext下面的包都是通过这个类加载器加载的,这个包下面的类基本上是以javax开头的。
System Class Loader
System Class Loader类加载器也叫应用程序类加载器(AppClassLoader)。顾名思义,这个类加载器就是用来加载开发人员自己平时写的应用代码的类的。System类加载器是用于加载存放在classpath路径下的那些应用程序级别的类的。
下面的代码列举出了这三个类加载器:
public class MainClass {
public static void main(String[] args) {
System.out.println(Integer.class.getClassLoader());
System.out.println(Logging.class.getClassLoader());
System.out.println(MainClass.class.getClassLoader());
}
}
其中获取Bootstrap类加载器永远返回null值
null # Bootstrap类加载器
sun.misc.Launcher$ExtClassLoader@5e2de80c # Extension类加载器
sun.misc.Launcher$AppClassLoader@18b4aac2 # System类加载器
双亲委派模型
上面介绍的三种类加载器,并不是孤立的,他们之间有一个层次关系:
三个类加载器之间通过这个层次关系协同工作,一起负责类的加载工作。上面的这种层次模型称为类加载器的“双亲委派”模型。双亲委派模型要求,除了最顶层的Bootstrap类加载器之外,所有的类加载器都必须有一个parent加载器。当类加载器加载类的时候,首先检查缓存中是否有已经被加载的类。如果没有,则优先委托它的父加载器去加载这个类,父加载器执行和前面子加载器一样的工作,直到请求达到顶层的Bootstrap类加载器。如果父加载器不能加载需要的类,那么这个时候才会让子加载器自己去尝试加载这个类。工作原理类似于下面这种方式:
我们可以通过JDK里ClassLoader里面的代码一窥双亲委派机制的实现方式,代码实现在ClassLoader.loadClass()里面
rotected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
} if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
采用双亲委派的方式组织类加载器,一个好处是为了安全。如果我们自己定义了一个String类,希望将这个String类替换掉默认Java中的java.lang.String的实现。
我们将自己实现的String类的class文件放到classpath路径下,当我们使用类加载器去加载我们实现的String类的时候,首先,类加载器会将请求委托给父加载器,通过层层委派,最终由Bootstrap类加载器加载rt.jar包里的String类型,然后一路返回给我们。在这个过程中,我们的类加载器忽略掉了我们放在classpath中自定义的String类。
如果没有采用双亲委派机制,那么System类加载器可以在classpath路径中找到String的class文件并加载到程序中,导致JDK中的String实现被覆盖。所以类加载器的这种工作方式,在一定程度上保证了Java程序可以安全稳定的运行。
线程上下文类加载器
上面讲了那么多类加载器相关的内容,可还是没有讲到今天的主题,线程上下文类加载器。
到这里,我们已经知道Java提供了三种类加载器,并且按照严格的双亲委派机制协同工作。表面上,似乎很完美,但正是这种严格的双亲委派机制导致在加载类的时候,存在一些局限性。
当我们更加基础的框架需要用到应用层面的类的时候,只有当这个类是在我们当前框架使用的类加载器可以加载的情况下我们才能用到这些类。换句话说,我们不能使用当前类加载器的子加载器加载的类。这个限制就是双亲委派机制导致的,因为类加载请求的委派是单向的。
虽然这种情况不多,但是还是会有这种需求。比较典型的,JNDI服务。JNDI提供了查询资源的接口,但是具体实现由不同的厂商实现。这个时候,JNDI的代码是由JVM的Bootstrap类加载器加载,但是具体的实现是用户提供的JDK之外的代码,所以只能由System类加载器或者其他用户自定义的类加载器去加载,在双亲委派的机制下,JNDI获取不到JNDI的SPI的实现。
为了解决这个问题,引入了线程上下文类加载器。通过java.lang.Thread类的setContextClassLoader()设置当前线程的上下文类加载器(如果没有设置,默认会从父线程中继承,如果程序没有设置过,则默认是System类加载器)。有了线程上下文类加载器,应用程序就可以通过java.lang.Thread.setContextClassLoader()将应用程序使用的类加载器传递给使用更顶层类加载器的代码。比如上面的JNDI服务,就可以利用这种方式获取到可以加载SPI实现的类加载器,获取需要的SPI实现类。
可以看到,引入线程类加载器实际是对双亲委派机制的破坏,但是却提供了类加载的灵活性。
解惑
回到开头,框架的代码为了加载框架之外用户实现的类,由于这些类可能没法通过框架使用的类加载器进行加载,为了绕过类加载器的双亲委派模型,采用Thread.getContextClassLoader()的方式去加载这些类。
Java的ThreadContext类加载器的实现的更多相关文章
- Java的ThreadContext类加载器
疑惑 以前在看源码的时候,总是会遇到框架里的代码使用Thread.currentThread.getContextClassLoader()获取当前线程的Context类加载器,通过这个Context ...
- 黑马程序员——【Java高新技术】——类加载器
---------- android培训.java培训.期待与您交流! ---------- 一.概述 (一)类加载器(class loader) 用来动态加载Java类的工具,它本身也是Java类. ...
- Java中的类加载器
转载:http://blog.csdn.net/zhangjg_blog/article/details/16102131 从java的动态性到类加载机制 我们知道,Java是一种动态语言.那么怎 ...
- Java中的类加载器以及Tomcat的类加载机制
在加载阶段,虚拟机需要完成以下三件事情: 1.通过一个类的全限定名来获取其定义的二进制字节流. 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构. 3.在Java堆中生成一个代表这个类 ...
- 黑马程序员:Java基础总结----类加载器
黑马程序员:Java基础总结 类加载器 ASP.Net+Android+IO开发 . .Net培训 .期待与您交流! 类加载器 Java虚拟机中可以安装多个类加载器,系统默认三个主要类加载器,每个 ...
- Java中的类加载器--Class loader
学习一下Java中的类加载器,这个是比较底层的东西,好好学习.理解一下. 一.类加载器的介绍 1.类加载器:就是加载类的工具,在java程序中用到一个类,java虚拟机首先要把这个类的字节码加载到内 ...
- Java基础加强-类加载器
/*类加载器*/ 把.class文件从硬盘上加载出来,将类的字节码(二进制)加载到内存中 /*类加载器及其委托机制*/ Java虚拟机中可以安装多个类加载器(可以自己编写),系统默认三个主要类加载器, ...
- JAVA基础_类加载器
什么是类加载器 类加载器是Java语言在1.0版本就引入的.最初是为了满足JavaApplet需要.现在类加载器在Web容器和OSGI中得到了广泛的应用,一般来说,Java应用的开发人员不需要直接同类 ...
- Java基础之类加载器
Java类加载器是用户程序和JVM虚拟机之间的桥梁,在Java程序中起了至关重要的作用,理解它有利于我们写出更优雅的程序.本文首先介绍了Java虚拟机加载程序的过程,简述了Java类加载器的加载方式( ...
随机推荐
- socket连接和http连接的区别
socket连接和http连接的区别 HTTP协议:简单对象访问协议,对应于应用层 ,HTTP协议是基于TCP连接的 tcp协议: 对应于传输层 ip协议: 对应于网络层 TCP/IP ...
- unity macro 分平台处理
https://docs.unity3d.com/ScriptReference/SystemInfo.html https://docs.unity3d.com/Manual/PlatformDep ...
- IP地址转换、主机大小端、htonl、ntohl实现
copy #include <IOSTREAM> //#include <WINSOCK.H> using std; typedef uint16; unsigned ...
- 教你用 google-drive-ocamlfuse 在 Linux 上挂载 Google Drive
如果你在找一个方便的方式在 Linux 机器上挂载你的 Google Drive 文件夹, Jack Wallen 将教你怎么使用 google-drive-ocamlfuse 来挂载 Google ...
- JMS与Spring之一(用JmsTemplate同步收发消息) --转
转自:http://blog.csdn.net/moonsheep_liu/article/details/6683190
- 倍福TwinCAT(贝福Beckhoff)常见问题(FAQ)如何修改标准驱动器编码器分辨率
在某个轴的Enc上双击,可以修改Scaling Factor Numerator 更多教学视频和资料下载,欢迎关注以下信息: 我的优酷空间: http://i.youku.com/acetao ...
- Python 二维码解码
二维码解析 Python中关于二维码解析的现成模块有很多,比较著名的就是Zbar以及ZXing.然而很不幸的是,官方的版本都是支持到python2.x,下面是在python2.x的例子: import ...
- webDriver API——第12部分WebElement
class selenium.webdriver.remote.webelement.WebElement(parent, id_) Bases: object Represents a DOM el ...
- 运用Unity结合PolicyInjection实现拦截器[结合操作日志实例]
上一篇文章我们通过Unity自身Unity.InterceptionExtension.IInterceptionBehavior实现一个有系统关异常日志记录:解决代码中到处充满的异常记录的代码: 本 ...
- 调整 firefox 源代码查看器的字体
默认的 firefox 源代码查看器的字体很不好看,不适合阅读代码.想要修改,又没有发现入口.如何修改呢?实际上在选项里的字体就能改.方法如下: 打开选项页,在字体一栏点击高级,把等宽字体由“宋体”改 ...