本文逻辑
- 本文首先介绍了
ContextClassLoader
的作用,然后说明了Thread
类里默认的类加载器是哪个? - 接着讲了
ContextClassLoader
的用法,然后举了个例子,并由这个例子引出了问题?为什么要打破委托机制?怎样打破委托机制? - 接着我们看了
ServiceLoader
的源码,看看它内部是怎么使用ContextClassLoader
来解决问题的。 - 最后,我们修改了线程的默认类加载器,发现
ServiceLoader
失效了,进一步验证了我们的文章!
具体内容
ContextClassLoader
是一种与线程相关的类加载器,类似 ThreadLocal
,每个线程对应一个上下文类加载器,主要是用了打破类加载器中的委托机制的。
类 java.lang.Thread
中的方法 getContextClassLoader()
和 setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是AppClassLoad
。在线程中运行的代码可以通过此类加载器来加载类和资源。
在使用时,一般都用下面的经典结构。
1 | //获取当前线程上下文类加载器 |
首先获取当前线程的线程上下文类加载器并保存到方法栈,然后设置当前线程上下文类加器为自己的类加载器。
doSomething 里面则调用了 Thread.currentThread().getContextClassLoader(),获取当前线程上下文类加载器做某些事情。
最后在设置当前线程上下文类加载器为老的类加载器。
为什么要这样设置呢?
具体例子:
1 | ServiceLoader<Driver> load = ServiceLoader.load(Driver.class); |
这个例子的输出结果是:
1 | driver: class org.h2.Driverloader: sun.misc.Launcher$AppClassLoader@18b4aac2 |
从这个执行结果里,我们可以看出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
,说到这里,终于把前一篇文章一直到这里给串起来了。
到这里再回想下:
ContextClassLoader
的作用是为了破坏 Java 类加载委托机制。- JDBC 规范定义了一个 JDBC 接口,然后使用 SPI 机制提供的一个叫做
ServiceLoader
的 Java 核心 API(rt.jar 里面提供)用来扫描服务实现类,它的加载器很明显是Bootstrap ClassLoad
。 - 服务实现者提供的 Jar,比如 MySQL 驱动则是放到我们的 classpath 下面,从上文知道默认线程上下文类加载器就是
AppClassLoader
,所以例子里面没有显式的在调用ServiceLoader
前设置线程上下文类加载器为AppClassLoader
,ServiceLoader
内部则获取当前线程上下文类加载器(这里为 AppClassLoader)来加载服务实现者的类,这里加载了 classpath 下的MySQL 的驱动实现。
这里关于具体的ServiceLoader
内部源码,我们简单的把重要的几行弄出来,就很明显了:
1 | public final class ServiceLoader<S> implements Iterable<S> { |
在代码5的地方,我们看到获取了线程的ContextClassLoader
,这里很明显类加载器是AppClassLoad
。所以在ServiceLoader
内部实际上帮我们做了这件事了,不用我们再显式的去修改。
到这里,最后了我们再玩一把,我们把线程默认的类加载器给它改掉,看看它会怎么样,具体代码如下:
1 | // (7) |
在代码(7)的部分,我们把默认线程的ContextCLassLoad设置成ExtClassLoader
,最后结果是:
1 | current thread context loader: sun.misc.Launcher$ExtClassLoader@7cca494b |
很明显对比之前的结果,类加载器加载不到Driver
这个类了,因为此时使用 ExtclassLoder 去查找 JDBC 驱动实现,而 ExtclassLoder 扫描类的路径为 JAVA_HOME/jre/lib/ext/,而这下面没有驱动实现的 Jar,所以不会查找到驱动。