神奇两行代码解决 Yaml 的类加载器大坑

366 阅读4分钟

废话不多说,直接描述问题。

下面这段简单的代码,却报了一个诡异的 ClassNotFound问题。

Yaml yaml = new Yaml();
Person person = new Person();
String data = yaml.dumpAsMap(person);
Person another = yaml.loadAs(data, Person.class);
System.out.println(another.equals(person));

image.png

说它诡异,是因为 Person 类明显是存在的,因为参数本身就是 Person.class 啊,在前一行也有一个成功的 new Person()操作。

询问 AI + 搜索,给出了一堆含糊不清的答案,坚持要让我检查各种类加载机制、CLASSPATH 路径等,我跟它说这些都没问题,它硬是不听啊!

神奇的两行代码

折腾好长时间没有效果,感觉有些沮丧,只能硬着头皮找组长解决了。组长看了一眼,淡定地加上了两行代码,然后竟然就可以运行了!

Thread.currentThread().setContextClassLoader(Person.class.getClassLoader());
Person another = yaml.loadAs(data, Person.class);
Thread.currentThread().setContextClassLoader(origin);

没错,只需要设置一下上下文类加载器,这个问题就解决了。

组长大致解释了一下原理,原来项目中使用了自定义的类加载器。Yaml 类是父加载器加载的,而 Person 类是子加载器加载的,当 Yaml 类在运行时,它需要利用子加载器去构造子类,但为了避免在函数中,层层传递 ClassLoader 参数(这样显得很丑),于是就使用了上下文类加载器这样的机制去传递变量。

这个我能理解,有点类似于 Spring Security 项目中为了传递一些用户信息,而又避免污染函数参数,采用了ThreadLocal一样。

我不能理解的是,我已经把 Person.class 参数传递给你了,你直接拿过去 new 一下不行吗?为什么还要搞得这么麻烦呢?

这个……Yaml 又不是我写的,我这么知道它要这样设计,你还是跟一下源码吧,搞清楚整个数据演变的逻辑。

追踪参数 Person.class 的演变过程

image.png Yaml 的反序列化,主要是loadAs这个函数执行的,其 type 参数是一个 Class 对象。

我们运行一下,画个动态序列图,可以看到,这个 Class 对象的 Id 尾数为 5215。

接下来,我们就像浏览地图一样,一步步跟踪 5215 对象的来龙去脉。

不过,你不需要肉眼去跟踪,这样容易迷路。

右键“show object”,可以自动查找所有跟这个对象相关的函数调用!

image.png 看看效果,红色数字和线条标记的函数调用,说明其参数或者返回值是 object ’514455215‘。

image.png 接下来就非常简单了,逐个点开各个函数,看一下源码和右边栏的参数/返回值详情,可以快速搞懂整个演变过程。

Class → String

首先,可以看到,在创建 Tag 对象(<>表示对象创建)时,调用Class 的 getName,转变成了一个 String 对象。

image.png

String → Class

然后,在 getClassForNode 中,又把 name 获取出来,再通过这个 name 去获取 Class 对象。

image.png

getClassForName

最后,我们看一下这个具体的获取逻辑,就是在这里,使用了高大上的上下文类加载器。

image.png 是不是很简单,如果你还不知道这个源码跟踪方法,就要小心落后了!

为什么不直接使用 Person.class 构造对象

流程虽然清楚了,但是我们的问题,似乎还没有答案,为啥 Yaml 不直接使用 Person.class 去构造对象呢?

为了回答这个问题,我们需要一个复杂一点的案例,在Person类中,再增加一个 Police 类,类似于下面这样:

public class Person {
    private String name;
    private int age;
    private Police police;
}

然后再跑一下流程,可以看到,整个反序列化的过程就是一个嵌套循环:

  • constrcutObject,newInstance
  • 获取属性的类型,然后 setType 到 子 Node 中
  • 针对子 Node,递归调用 constructObject 得到value

image.png 在这个过程中,我们可以看到,Person.class 通过 Class.forName 加载出来的,而 Police.class 则是通过反射 Person.class 获得的,整个过程,其实只用了一次 Class.forName。

也就是说,如果一开始就直接调用 Person.class 去 new 对象,整个流程是完全可以跑通的。根本没必要引入这么高大上的上下文类加载器。

类似的代码,我测试了一下 Jackson 组件,根本就没有这个问题。 不得不说,Yaml 这真是挖了一个大坑。

参考:

  1. xcodemap