关于上下文类加载器,之前一直只是了解,在开发过程中并未使用过,直到开发 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
,此时就无法加载位于应用程序类路径中的类了,所以,上下文类加载器就是为了解决该问题,支持根据当前线程的上下文类加载器去进行服务实现的加载,以规避双亲委派模型无法加载实现类的问题。
Reference
ServiceLoader (Java Platform SE 8 )
Java Classloader - Wikipedia