关于FastJson对构造函数的调用研究

没有构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package src.main.fastjsonconstructor;

import com.alibaba.fastjson.JSON;

public class noConstructor{
public String name;
public String student;
public String id;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getStudent() {
return student;
}

public void setStudent(String student) {
this.student = student;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", id='" + id + '\'' +
", student='" + student + '\'' +
'}';
}
public static void main(String[] args) {
String payload = "{\"name\":\"123\",\"student\":\"aaa\",\"id\":\"456\"}";
noConstructor no = JSON.parseObject(payload, noConstructor.class);
System.out.println(no);
}
}

输出

1
User{name='123', id='456', student='aaa'}

这是属于fj比较常见的一种情况,由于没有构造函数,构造函数在编译期间由编译器自动生成的一个无参构造函数,再通过调用对应json键值的getter/setter函数进行赋值,如果有无参构造函数的话,就会直接调用无参构造函数

存在有参构造函数

我们对上边的noConstructor类增加一个有参构造函数,再执行该代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package src.main.fastjsonconstructor;

import com.alibaba.fastjson.JSON;

public class noConstructor{
public String name;
public String student;
public String id;

public noConstructor(String name,String id){
this.name = name;
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getStudent() {
return student;
}

public void setStudent(String student) {
this.student = student;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", id='" + id + '\'' +
", student='" + student + '\'' +
'}';
}
public static void main(String[] args) {
String payload = "{\"name\":\"123\",\"student\":\"aaa\",\"id\":\"456\"}";
noConstructor no = JSON.parseObject(payload, noConstructor.class);
System.out.println(no);
}
}

输出

1
User{name='123', id='456', student='null'}

可以发现这里调用了有参构造函数,并且并没有给student赋值,当我们手动为类添加有参构造函数时候,在编译器编译时,就不会再为其添加无参构造函数,也就是说你的类有且只有你添加的这个构造函数。那这样FastJson是如何反序列化生成实例的呢?

调用分析

image-20240410160331290

这次要讲的重点在JavaBeanInfo的build方法中。从类名中大奖可以知道,这是FastJson内部存储反序列化对象信息的类。这其中就包含了创建该类的构造方法信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static Constructor<?> getDefaultConstructor(Class<?> clazz, final Constructor<?>[] constructors) {
if (Modifier.isAbstract(clazz.getModifiers())) {
return null;
}

Constructor<?> defaultConstructor = null;
//这里很好理解 先遍历下所有的构造函数,找到其中无参构造函数
for (Constructor<?> constructor : constructors) {
if (constructor.getParameterTypes().length == 0) {
defaultConstructor = constructor;
break;
}
}

if (defaultConstructor == null) {
if (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())) {
Class<?>[] types;
for (Constructor<?> constructor : constructors) {
if ((types = constructor.getParameterTypes()).length == 1
&& types[0].equals(clazz.getDeclaringClass())) {
defaultConstructor = constructor;
break;
}
}
}
}

return defaultConstructor;
}

接下来使用无参构造函数进行反序列化

从调试状态的信息可以看到默认构造函数是无参构造函数,默认构造函数的参数长度为0个。

下面是有参构造函数的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boolean is_public = (constructor.getModifiers() & Modifier.PUBLIC) != 0;
if (!is_public) {
continue;
}
//前面的方法都是进行一些过滤 下面的才是获取手动有参构造函数的关键。
//首先会获取构造函数的参数名称列表 如果参数列表为空或者长度为0 则放弃该方法,开始下一次循环
//这里的获取参数名称使用的并不是java8中提供的获取方法参数名称的方式
//而是通过流读取class文件的的方式来获取的
String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);
if (lookupParameterNames == null || lookupParameterNames.length == 0) {
continue;
}
//下面这段方法很显然就是在比较并交换,如果该有参构造函数的参数个数大于之前的构造方法中
//参数个数最多的构造方法,则用这个构造方法和参数名称数组替换之前保存的构造方法和参数名称数组
if (creatorConstructor != null&& paramNames != null && lookupParameterNames.length <= paramNames.length) {
continue;
}

上面的方法的作用是从所有的构造方法中获取参数最多的一个,并将其放入JaveBeanInfo的creatorConstructor属性中,供后面实例化对象使用。要特别注意一下,这里的获取参数名称使用的并不是java8中提供的获取方法参数名称的方式,而是通过流读取class文件的的方式来获取的。在赋值的时候,会通过参数名称去json串中查找对应名称的字段来赋值,并且在通过构造函数赋值完毕之后,将不再通过set方法赋值(这里有坑一定要记住,否则会出现赋值不上的莫名其妙的错误)。

调用流程图

image-20240410161751148

存在多个参数数量相同的构造函数(调用随机性)

试验环境jdk8u271 commons-io2.4

这里就以org.apache.commons.io.output.WriterOutputStream类为例进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public WriterOutputStream(Writer writer, CharsetDecoder decoder) {
this(writer, (CharsetDecoder)decoder, 1024, false);
}

public WriterOutputStream(Writer writer, CharsetDecoder decoder, int bufferSize, boolean writeImmediately) {
this.decoderIn = ByteBuffer.allocate(128);
this.writer = writer;
this.decoder = decoder;
this.writeImmediately = writeImmediately;
this.decoderOut = CharBuffer.allocate(bufferSize);
}

public WriterOutputStream(Writer writer, Charset charset, int bufferSize, boolean writeImmediately) {
this(writer, charset.newDecoder().onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE).replaceWith("?"), bufferSize, writeImmediately);
}

public WriterOutputStream(Writer writer, Charset charset) {
this(writer, (Charset)charset, 1024, false);
}

public WriterOutputStream(Writer writer, String charsetName, int bufferSize, boolean writeImmediately) {
this(writer, Charset.forName(charsetName), bufferSize, writeImmediately);
}

public WriterOutputStream(Writer writer, String charsetName) {
this(writer, (String)charsetName, 1024, false);
}

public WriterOutputStream(Writer writer) {
this(writer, (Charset)Charset.defaultCharset(), 1024, false);
}

上面就是该类的构造函数,足足有七个,按照上面的分析,这里应该会调用参数最多的构造函数,但参数最多(四个)构造函数有两个,这究竟会调用哪个呢?

这里我们继续跟进fj的JavaBeanInfo类的源码

1
lookupParameterNames.length > paramNames.length

刚刚已经说过,这个位置是判断构造函数的参数数量的,我们往回找

image-20240410162101563

就在正上方就找到了这个constructor,看名字大致就可以才出来这个应该就是我们的有参构造函数,那这个constructor是怎么来的?我们向上分析

image-20240410162323007

不难找到如上代码片段,这里循环从0将获得的var21赋值给constructor,猜测这里的var21应该是构造函数数组,我们继续向上分析

image-20240410162507466

就在上方,基本可以确认为构造函数数组,继续跟踪

image-20240410162558949

可以发现在289行,利用getDeclaredConstructors()获取了clazz的所有构造函数,这里的clazz就是我们传入的类,但这里就出现了一个问题,就是getDeclaredConstructors这个函数的获取随机性,这里仅仅是我个人的见解,暂时并没有去找其他文章作为理论支撑,我们用下面的代码进行测试

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws ClassNotFoundException {
String payload = "{\"@type\":\"java.lang.AutoCloseable\",\"@type\":\"org.apache.commons.io.output.WriterOutputStream\",\n" +
" \"file\":\"/tmp/pwned\",\n" +
" \"charset\":\"UTF-8\",\n" +
" \"append\": false}";
Class c = Class.forName("org.apache.commons.io.output.WriterOutputStream");
Constructor[] constructor = c.getDeclaredConstructors();
for (int i = 0; i < constructor.length; i++) {
System.out.println(constructor[i]);
}
JSON.parseObject(payload);
}

运行之后结果如下

image-20240410163311035

image-20240410163326104

发现出现了两种运行结果,这是为什么呢?简单说下测试代码,利用反射获取了其所有构造函数,然后循环输出。

我们观察控制台的输出结果,不难发现输出的顺序竟然有所不同,这就是我所说的调用的随机性,我们再回到上面所说的对于参数数量的判断部分

1
2
3
4
if (lookupParameterNames != null && lookupParameterNames.length != 0 && (creatorConstructor == null || paramNames == null || lookupParameterNames.length > paramNames.length)) {
paramNames = lookupParameterNames;
creatorConstructor = constructor;
}

这里的lookupParameterNames就是我们正在判断的构造函数的参数,paramNames是已经存在的构造函数的判断,由此可得,如果我们现在传入的构造函数的参数数量大于已知的最大(初始为0)的构造函数的参数数量,那么就会进行构造函数的覆盖。由于getDeclaredConstructors函数获取的构造参数顺序不同,假设其两个构造函数为c1和c2,如果获取的顺序为c1在前,那么在c2进入判断的时候,由于和c1的参数数量相等,所以就不会进行覆盖操作。所以,当存在多个参数数量相同的构造函数时,fj会始终调用最开始传入的那个构造函数。