关于上下文类加载器,之前一直只是了解,在开发过程中并未使用过,直到开发 Java Agent 时,才真正明白其作用,本文作简要记录。在 Java SPI 机制的核心类 ServiceLoader 中,有以下代码:
1 | /** |
可以看出,在创建服务加载器时用到了线程上下文类加载器,在 DriverManager 的 static 代码块中,就用到了该方法:
1 | /** |
关于此处为何要使用上下文类加载器,上面这个例子不能很好的解释这个原因,我只能根据开发 Java Agent 的经验做简要的解释,上下文类加载器主要是解决当前类所在的类加载器整个双亲委派层次上无法加载到类的问题。我们用 ServiceLoader 类举例,该类位于 <JAVA_HOME>/jre/lib 目录下的 rt.jar,由 Bootstrap ClassLoader 加载,如果查看 ServiceLoader 的源码,会发现最终去加载类时是使用的 LazyIterator 中的 nextService 方法去加载的类,部分源码如下:
1 | private S nextService() { |
其中调用的 Class.forName 方法是使用的含 ClassLoader 参数的重载版本,代码 c = Class.forName(cn, false, loader) 中的 loader 即为 Thread.currentThread().getContextClassLoader() 返回的当前线程上下文类加载器。现在,我们编写了一个简单的 Java Agent,整个 Java Agent 的源码位于 GitHub - tianshuang/agent-test at context-class-loader-not-set,该 Agent 采用了独立的类加载器,该独立的类加载器的父加载器为 Bootstrap ClassLoader,源码 AgentClassLoader.java 如下:
1 | package me.tianshuang.agent; |
该 Agent 的入口处 premain 核心代码位于 Agent.java :
1 | package me.tianshuang.agent; |
该 Java Agent 依赖了 log4j2 2.11.1 版本的依赖,此时,使用该 Java Agent 的应用也依赖了 log4j2 2.11.1 版本的依赖,AgentClassLoader 的父加载器为 Bootstrap ClassLoader,因为我们未设置过上下文类加载器,根据 Oracle 文档 Class Loading 中的描述,默认为加载应用程序的类加载器,即系统类加载器,那么,当执行到 nextService 方法时,从当前线程中获取到的上下文类加载器为 Launcher.AppClassLoader,即系统类加载器,而此时我们需要加载服务实现的接口 org.apache.logging.log4j.util.PropertySource 又是由 AgentClassLoader 加载的,我们使用了上下文类加载器 Launcher.AppClassLoader 去加载了服务的实现 org.apache.logging.log4j.util.EnvironmentPropertySource 且成功加载了该类,但是,在随后的 service.isAssignableFrom(c) 判断中,返回了 false,随即触发了 ServiceConfigurationError,即 ServiceLoader 不允许接口与实现类由不同的类加载器加载,此时会认为 EnvironmentPropertySource 不是 PropertySource 的子类型,异常信息如下:
1 | org.apache.logging.log4j.util.PropertySource: Provider org.apache.logging.log4j.util.EnvironmentPropertySource not a subtype |
虽然 EnvironmentPropertySource 实现了 PropertySource 接口,但是它们由不同的类加载器加载,那么就不符合 ServiceLoader 的规定,如果我们参考 OpenTelemetry 的 Java Agent 实现,我们会发现,它们在使用 Agent 独立的类加载器时,即将去初始化 Agent 的相关逻辑时,设置了上下文类加载器,最后在 finally 中恢复了当前线程之前的上下文类加载器,这就是上下文类加载器的作用,其源码位于 AgentInitializer.java,实现如下:
1 | // called via reflection in the OpenTelemetryAgent class |
简单来说,上下文类加载器在 ServiceLoader 中主要用于打破双亲委派模型,如果不使用上下文类加载器,那么默认 Class.forName 实现采用的调用方的类加载器,源码如下:
1 | /** |
而此时调用方 ServiceLoader 由 Bootstrap ClassLoader 类加载器加载,其对应的目录为 <JAVA_HOME>/jre/lib,此时就无法加载位于应用程序类路径中的类了,所以,上下文类加载器就是为了解决该问题,支持根据当前线程的上下文类加载器去进行服务实现的加载,以规避双亲委派模型无法加载实现类的问题。
References
ServiceLoader (Java Platform SE 8 )
Java Classloader - Wikipedia