title: 类加载机制(七):线程上下文类加载器
date: 2019-03-21 20:14:06
categories:
我们知道,class
文件的加载是按照双亲委托机制完成的,这个机制解决了各个类加载器的基础类的统一问题,因为上层类加载器加载的类对下层加载的类是可见的,所以这些基础类可以被Java
程序所调用,但是如果这些基础类需要调用用户所写的类呢,可下层类加载器加载的类不是对上层类加载器加载的类是透明的吗?这种情况不是不可能的,比如JDBC
,它位于rt.jar
包下,是由启动类加载器加载,而它却需要classPath
下的接口提供者(Service Provider Interface)的代码,但是启动类不可能去加载这些代码,那怎么办呢?
SPI(Service Provider Interface)
:
只提供了接口的声明,具体实现由厂商完成。某些SPI
需要调用由厂商实现并部署在classPath
下的接口实现代码。这些接口由启动类加载器去加载,但启动类加载器不认识classPath
下的代码。
这时候就需要对双亲委托机制进行“破坏”了,Java
设计者设计了一种叫做“线程上下文类加载器”的机制,当这些接口需要实现的代码时,就去使用这个线程上下文类加载器完成这些接口实现代码的加载。
线程上下文类加载器contextClassLoader
,其实,我们在分析Launcher
类的源码时,已经遇到过了:
Thread.currentThread().setContextClassLoader(this.loader);
在Launcher
类的构造方法中,执行了这条语句,并将系统类加载器传进去,我们跟着去看看Thread
的源码。
private ClassLoader contextClassLoader;
内部维护了一个类加载器,也就是线程上下文类加载器。
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
在Thread
的init
方法中,线程继承其父线程的上下文类加载器。
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
在Thread
中,可以设置线程类加载器,也就是说,若如果没有通过setContextClassLoader
进行设置的话,线程将继承其父线程的上下文类加载器。前面说过,在Launcher
类中设置了线程上下文类加载为系统类加载器,即在Java
程序中未设置线程上下文类加载器的话,线程上下文类加载器就为系统类加载器。
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try{
Thread.currentThread().setContextClassLoader(targetccl);
doMethod();
}finally{
Thread.currentThread().setContextClassLoader(classLoader);
}
线程上下文类加载器的一般使用模式(获取-使用-还原)
在
doMethod()
里面调用了Thread.currentThread().setContextClassLoader()
来获取当前线程的上下文类加载器做某些事情。
在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行加载。但是对于SPI
来说。有些接口是Java
核心库所提供的,而Java
核心库是由启动类加载器来加载的,而这些接口的实现却是来自于不同的jar
包(由各独立厂商实现),Java
的启动类加载器是不会加载其他来源的jar
包,这样就导致一些接口由启动类加载加载,实现由其他加载器加载,传统的双亲委托机制就会无法满足SPI
的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载 。就比如JDBC
驱动,如下图所示。
首先,在pom
文件中添加了关于MySql
的依赖,我们通过代码找到项目中的驱动。
public class MyTest17 {
public static void main(String[] args){
//通过ServiceLoader去加载驱动
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = drivers.iterator();
//打印出每个驱动以及驱动的类加载器
while (iterator.hasNext()){
Driver next = iterator.next();
System.out.println("driver:" + next.getClass());
System.out.println("loader:" + next.getClass().getClassLoader());
System.out.println("-------------");
}
//打印出当前线程上下文类加载器
System.out.println("当前线程上下文类加载器:" + Thread.currentThread().getContextClassLoader());
//打印出ServiceLoader的类加载器
System.out.println("ServiceLoader的类加载器:" + ServiceLoader.class.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
-------------
当前线程上下文类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader的类加载器:null
从输出结果,我们可以看到,
MySql
的两个驱动都是由系统类加载器所加载的,按常理来说,这些驱动位于classPath
下,也该由系统类记载器去加载;前面也说了,当前线程上下文类加载器默认是被设置为系统类加载器;而ServiceLoader
位于java.util
下,属于核心类库,是由启动类加载器加载的。
ServceLoader
是对加载驱动很重要的一个类,那它是如何找到这些驱动的呢?
ServiceLoader
是从JDK1.6
才开始出现的,它的作用就是定位加载这些服务的具体实现。从JavaDoc
中,我们可以看出ServiceLoader
是从服务实现的jar
包中的资源目录META-INF/services
下去读取驱动名的,并且要求包含这些驱动的文件名必须是服务类型的完全限定的二进制名字,且文件中包含的也是服务实现类的完全限定二进制名,如下图所示。
META-INF/services
下的java.sql.Driver
文件里的内容。
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
首先,看看ServiceLoader
类内部维护的重要字段。
private static final String PREFIX = "META-INF/services/";
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
直接将资源目录
META-INF/services/
写死在类中,因为这个资源目录是定好了的,写死也无所谓的。第二个字段是用做缓存的,
ServiceLoader
维护到目前为止加载的服务SPI
的缓存
然后,从load
方法说起。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
将服务类型与当前系统类加载器传给另一个
load
方法。
这里为什么要获取线程上下文类加载器呢?分析一下:ServiceLoader
是在普通的Java
程序中被引用到的,我们知道,在一个类X
中引用另一个类Y
,是使用X
的类加载器去尝试加载类Y
。
系统类加载器会去尝试加载ServiceLoader
最终启动类加载器将其加载
在ServiceLoader
中引用到的类,会由启动类加载器去加载
但启动类加载器是无法加载到classPath
下的类的,所以就通过线程上下文类加载器去加载
接着,调用私有的构造方法。
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
//要是传进来的线程类加载器为空,就将cl设置为系统类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
//调用reload
reload();
}
public void reload() {
//将上文提到的缓存清空
providers.clear();
//使用一个内部类进行懒加载
lookupIterator = new LazyIterator(service, loader);
}
ServiceLoader
内部加载这些服务实现类,是通过按需加载的方式。
只贴出这个内部类LazyIterator
的主要代码。
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
在这个内部类内部最重要的两个方法,
hasNextService
判断是否还有下一个服务实现类,nextService
去加载服务实现类。
public class MyTest17 {
public static void main(String[] args){ Thread.currentThread().setContextClassLoader(MyTest17.class.getClassLoader().getParent());
ServiceLoader<Driver> drivers = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = drivers.iterator();
while (iterator.hasNext()){
Driver next = iterator.next();
System.out.println("driver:" + next.getClass());
System.out.println("loader:" + next.getClass().getClassLoader());
System.out.println("-------------");
}
System.out.println("当前线程上下文类加载器:" + Thread.currentThread().getContextClassLoader());
System.out.println("ServiceLoader的类加载器:" + ServiceLoader.class.getClassLoader());
}
}
输出结果:
当前线程上下文类加载器:sun.misc.Launcher$ExtClassLoader@1540e19d
ServiceLoader的类加载器:null
可以看到,我们将线程类加载器设置为扩展类加载器后,确实是无法将
classPath
下的服务实现类加载成功的。
我们将线程类加载器设置为我们的自定义类加载器。
Thread.currentThread().setContextClassLoader(new MyClassLoader("loader"));
输出结果:
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
-------------
当前线程上下文类加载器:classLoader.MyClassLoader@6d6f6e28
ServiceLoader的类加载器:null