双亲委派机制的真实案例

双亲委派机制

双亲委派是 JAVA 类加载器加载类的一种机制,其工作方式是:当需要加载一个类时,这个类加载器首先会递归地委托其父加载器去加载,当父加载器找不到这个类时,才尝试自己去加载。

也就是说,类加载器,也有父子关系,如图所示(截图取自《深入理解 JAVA 虚拟机》)

mR4hKe.jpg

其中,Bootstrap ClassLoader 是顶层的启动类加载器,除它之外,所有的类加载器都有自己的父加载器。

案例分享

bug 表现

最近的工作中,使用了 java agent 获取应用的部分数据,包括一些 spring 的信息,具体方式可以参考java探针技术II——如何在不依赖 spring 的情况下,使用 spring。因此,agent 的 jar 包中是未引入 spring 相关的任何包的,代码中使用到相关类时,会采用 Class.forName() API。

1
Class clazz = Class.forName("org.springframework.core.env.EnumerablePropertySource");

这个 API 有很多异常需要捕获,包括 ClassNotFoundException,不过通常情况下不会发生。

本地运行时,agent 附着到一个 springboot 框架的简单应用中,并运行完好,没有异常。而奇怪的是,当部署到公司平台的容器中,这段代码会报错 ClassNotFoundException。

1
2
2019-08-26 10:34:39 5f849f8c7d-6zj9g-dev_bf93ce3b-c4bd-11e9-a3b2-0242ac120003_0 ERROR: reflect error: java.lang.ClassNotFoundException:
org.springframework.core.env.EnumerablePropertySource

排查过程

一开始理所当然地认为,是不是 jar 包没有正确地打包到容器中,或者被覆盖之类的情况。虽然我认为,如果 jar 包真的出问题(spring-core),那么应该会直接运行不了。而当时的表现是,除了使用这一部分代码的地方,其余运行完好,没有任何报错。

还是顺着这个方向排查了,首先在本地容器化运行,成功复现 bug。

进入本地容器中,查看该 jar 包,的确存在,没有问题。

本地 jar 包运行,也复现了这个 bug。

到这里为止似乎没有了头绪,然后灵光一闪突然回归到这个错误本身 ClassNotFoundException,这是出现在类加载器加载类时,找不到这个 class。而已经检查过 class 没有问题了,或许是类加载器的问题呢。

解决思路

双亲委派机制其实一直都知道,说起来也知道工作原理,可是仿佛并未真正理解。

一开始没有想到这个问题,也是由于,第一次写 java agent,虽然知道跟普通 java 应用有区别,但本地没有报错似乎就觉得没有问题了,同时没有把探针与类加载器挂上钩。

想到这里之后,首先去查询了官方文档,看到这样一句话

The agent class will be loaded by the system class loader (see ClassLoader.getSystemClassLoader)).

系统类加载器就是上图中的应用程序类加载器,由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 的返回值,因此也被称为系统类加载器。

接下来,只要获取加载 spring 框架的类加载器,就可以得到答案了。

在报错代码前加上如下代码

1
2
Logger.info("agent class loader is [{}]", this.getClass().getClassLoader());
Logger.info("spring class loader is [{}]", ApplicationContextHolder.applicationContextObj.getClass().getClassLoader());

ApplicationContextHolder.applicationContextObj 是当 spring 加载成功后,获取到的 spring context ,保存在静态变量中

本地运行时得到结果
1
2
3
4
···
INFO: spring class loader is [sun.misc.Launcher$AppClassLoader@18b4aac2]
INFO: agent class loader is [sun.misc.Launcher$AppClassLoader@18b4aac2]
···
jar 包运行时得到结果
1
2
3
4
···
INFO: agent class loader is [sun.misc.Launcher$AppClassLoader@18b4aac2]
INFO: spring class loader is [org.springframework.boot.loader.LaunchedURLClassLoader@6156496]
···

联合双亲委派机制的工作原理得到结论,在本地运行时,由于 agent 与 spring 的类加载器是一样的,因此不会出现 ClassNotFoundException 这个异常。而在 jar 包运行时,由于 spring-core 是以额外的 jar 引入的,使用了 URLClassLoader ,它的级别比 AppClassLoader 低。所以 agent 在寻找这个类时,只会递归委托其父类加载器加载,而不会向下得到其子类加载器加载的类,因此也就出现异常了。