1. 线程上下文类加载器相关概念
当前类加载器(Current Classloader): 用户加载当前类的类加载器。
每个类都会使用自己的类加载器(即加载自身的类加载器)去加载其他的类(自身所依赖的类)。如果ClassX 引用了ClassY, 那么ClassX的类加载器就会去加载ClassY(前提是ClassY还未被加载)。
线程上下文类加载器:
这个概念从JDK1.2引入, 它指的值Thread类的实例变量contextClassLoader, 对应的方法是
Thread.setContextClassLoader(ClassLoader cl) 和 public ClassLoader getContextClassLoader(), 分别用来设置和获取线程上下文类加载器。
如果没有通过setContextClassLoader方法设置的话,线程将继承父线程的上下文类加载器。
Java应用运行时的初始线程的线程上下文类加载是应用类加载器。
可以通过简单的代码示例佐证:
package classloader4;
public class MyTest27 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader());
System.out.println(Thread.class.getClassLoader());
}
}
运行结果:
sun.misc.Launcher$AppClassLoader@18b4aac2
null
2. 为什么引入线程上下文类加载器
在双亲委托模型下,类加载是由下至上的,Java的启动类加载器不会加载其他来源的Jar包,而SPI的实现类是在classpath下,启动类加载器是无法加载的。
举个例子,java.sql.Connection 接口定义了数据库连接的规范和标准,它是在rt.jar包中,是由根类加载器来加载的。不同的数据库厂商对于数据库连接的具体实现不同,比如java访问MySQL数据库需要引入MySQL驱动jar包,它存在于classpath下,根类加载器是加载不了的, 但是java.sql.DriverManager又要用到Driver接口的实例,这个时候就会出现问题。
这里要区分上一节里的一个结论: 父类加载器加载的类不能看见子类加载器加载的类,这里强调的是父类加载器所加载的类里直接访问了子类加载器所加载的类(具体的一个类)的情况。
但SPI的例子里,java.sql.DriverManager类的定义里,需要访问到 java.sql.Driver接口的实例, 但Driver接口的实例所对应的真实的类不是由父类加载器所加载的,是没有问题的。
为了解决诸如此类的SPI类加载的问题,Java引入了线程上下文类加载器。父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classLoader所加载的类。 这就改变了双薪委托模型。
3. 如何使用线程上下文类加载器
线程上下文类加载器的使用方式为:获取 - 使用 - 还原
// 获取
ClassLoader cl = Thread.currentThread().getContextClassLoader();
try{
Thread.currentThread().setContextClassLoader(targetTccl);
//do something
}
finally {
// 还原
Thread.currentThread().setContextClassLoader(cl);
}
4. 关于java.util.ServiceLoader
java.util.ServiceLoader类自JDK1.6引入,它的一个简单的服务提供者加载工具。
如果我们的工程依赖了mysql的驱动的jar包到classpath目录下,我们执行如下程序:
package classloader3;
import java.sql.Driver;
import java.util.ServiceLoader;
public class MyTest26 {
public static void main(String[] args) {
ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
for (Driver driver : serviceLoader) {
System.out.println("driver:" + driver.getClass() + ", loader: " + driver.getClass().getClassLoader());
}
}
}
最后的输出结果是这样:
driver:class com.mysql.jdbc.Driver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2
driver:class com.mysql.fabric.jdbc.FabricMySQLDriver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到,ServiceLoader
容器里已经有Driver类的具体实例了。这是因为ServiceLoader类回去classpath路径下的resources/META-INF/services/ 目录下寻找按照一定规范定义的文件(文件名是接口的全名,文件内容是接口对应实现类的全名,可以多个),并通过线程上下文类加载器去加载定义类的实例。如果想了解更多细节可以查看源码,很简单的。
看下mysql 驱动jar包是如何定义这个文件的:
这个时候在对比上面例子的输入结果,也就不难理解了。
5. 自验证代码模拟
demo代码:https://github.com/dchangjian/jvm_study/blob/master/dcj/src/main/java/classloader4/MyPersonTest.java
5. 结论
- ContextClassLoader的作用就是为了破坏Java的类的双亲委托机制
- 当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)低层的类时,就必须通过上下文类加载器来帮助高层的ClassLoader找到并加载该类