详解常用类加载器:ContextClassLoader

本文逻辑

  1. 本文首先介绍了ContextClassLoader的作用,然后说明了Thread类里默认的类加载器是哪个?
  2. 接着讲了ContextClassLoader的用法,然后举了个例子,并由这个例子引出了问题?为什么要打破委托机制?怎样打破委托机制?
  3. 接着我们看了ServiceLoader的源码,看看它内部是怎么使用ContextClassLoader来解决问题的。
  4. 最后,我们修改了线程的默认类加载器,发现ServiceLoader失效了,进一步验证了我们的文章!

具体内容

ContextClassLoader 是一种与线程相关的类加载器,类似 ThreadLocal,每个线程对应一个上下文类加载器,主要是用了打破类加载器中的委托机制的。

java.lang.Thread中的方法 getContextClassLoader()setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是AppClassLoad。在线程中运行的代码可以通过此类加载器来加载类和资源。

在使用时,一般都用下面的经典结构。

1
2
3
4
5
6
7
8
9
//获取当前线程上下文类加载器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {//设置当前线程上下文类加载器为targetTccl
Thread.currentThread().setContextClassLoader(targetTccl);
//doSomething
doSomething();
} finally {//设置当前线程上下文加载器为原始加载器
Thread.currentThread().setContextClassLoader(classLoader);
}

首先获取当前线程的线程上下文类加载器并保存到方法栈,然后设置当前线程上下文类加器为自己的类加载器。

doSomething 里面则调用了 Thread.currentThread().getContextClassLoader(),获取当前线程上下文类加载器做某些事情。

最后在设置当前线程上下文类加载器为老的类加载器。

为什么要这样设置呢?
具体例子:

1
2
3
4
5
6
7
8
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = load.iterator();
while (iterator.hasNext()) {
Driver next = iterator.next();
System.out.println("driver: " + next.getClass() + "loader: " + next.getClass().getClassLoader());
}
System.out.println("current thread context loader: " + Thread.currentThread().getContextClassLoader() );
System.out.println("ServiceLoader loader: " + ServiceLoader.class.getClassLoader());

这个例子的输出结果是:

1
2
3
driver: class org.h2.Driverloader: sun.misc.Launcher$AppClassLoader@18b4aac2
current thread context loader: sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader loader: null

从这个执行结果里,我们可以看出ServiceLoader 是用的Bootstarp class load 进行加载的,所以它的输出结果是null

com.mysql.jdbc.Driver 使用AppClassLoad 进行加载的。根据下面这句话:

1
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);

我们知道一个类中如果引用了另外一个类,那么被引用的类应该也由引用方的类加载进行加载。在这句话中,引用方是ServiceLoader,被引用方是Driver,按这样来说,Driver应该使用Bootstarp class load进行加载的,但实际我们也能看到,用的是APPClassLoader进行加载的。

这就很奇怪,感觉不满足我们的委托机制!这个就是问题。

既然有了问题,我们考虑为啥要这样做?打破了这个委托机制到底有什么好处?
如果不打破这个委托机制,我们的Driver就要用Bootstarp ClassLoader进行加载,但是我们通过上篇文章也知道,Bootstarp ClassLoader是用来加载JVM核心类库的,这种类不应当使用它来加载,因为这个路径下面没有我们Driver实现的类,所以肯定是找不到的,但是委托机制的存在又让我们不得不用它来加载,所以需要打破这种委托机制,而在这里打破的方式就是我们的ContextClassLoader,说到这里,终于把前一篇文章一直到这里给串起来了。

到这里再回想下:

  1. ContextClassLoader 的作用是为了破坏 Java 类加载委托机制。
  2. JDBC 规范定义了一个 JDBC 接口,然后使用 SPI 机制提供的一个叫做 ServiceLoader 的 Java 核心 API(rt.jar 里面提供)用来扫描服务实现类,它的加载器很明显是Bootstrap ClassLoad
  3. 服务实现者提供的 Jar,比如 MySQL 驱动则是放到我们的 classpath 下面,从上文知道默认线程上下文类加载器就是 AppClassLoader,所以例子里面没有显式的在调用 ServiceLoader 前设置线程上下文类加载器为 AppClassLoaderServiceLoader 内部则获取当前线程上下文类加载器(这里为 AppClassLoader)来加载服务实现者的类,这里加载了 classpath 下的MySQL 的驱动实现。

这里关于具体的ServiceLoader内部源码,我们简单的把重要的几行弄出来,就很明显了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final class ServiceLoader<S> implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) {
// (5)获取当前线程上下文加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
//(6)
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}

在代码5的地方,我们看到获取了线程的ContextClassLoader,这里很明显类加载器是AppClassLoad。所以在ServiceLoader内部实际上帮我们做了这件事了,不用我们再显式的去修改。

到这里,最后了我们再玩一把,我们把线程默认的类加载器给它改掉,看看它会怎么样,具体代码如下:

1
2
3
4
5
6
7
8
9
10
// (7)
Thread.currentThread().setContextClassLoader(ContextClassLoadTest.class.getClassLoader().getParent());
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
Iterator<Driver> iterator = load.iterator();
while (iterator.hasNext()) {
Driver next = iterator.next();
System.out.println("driver: " + next.getClass() + "loader: " + next.getClass().getClassLoader());
}
System.out.println("current thread context loader: " + Thread.currentThread().getContextClassLoader() );
System.out.println("ServiceLoader loader: " + ServiceLoader.class.getClassLoader());

在代码(7)的部分,我们把默认线程的ContextCLassLoad设置成ExtClassLoader,最后结果是:

1
2
current thread context loader: sun.misc.Launcher$ExtClassLoader@7cca494b
ServiceLoader loader: null

很明显对比之前的结果,类加载器加载不到Driver这个类了,因为此时使用 ExtclassLoder 去查找 JDBC 驱动实现,而 ExtclassLoder 扫描类的路径为 JAVA_HOME/jre/lib/ext/,而这下面没有驱动实现的 Jar,所以不会查找到驱动。