关于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是如何反序列化生成实例的呢?
调用分析
这次要讲的重点在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; }
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方法赋值(这里有坑一定要记住,否则会出现赋值不上的莫名其妙的错误)。
调用流程图
存在多个参数数量相同的构造函数(调用随机性)
试验环境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
|
刚刚已经说过,这个位置是判断构造函数的参数数量的,我们往回找
就在正上方就找到了这个constructor,看名字大致就可以才出来这个应该就是我们的有参构造函数,那这个constructor是怎么来的?我们向上分析
不难找到如上代码片段,这里循环从0将获得的var21赋值给constructor,猜测这里的var21应该是构造函数数组,我们继续向上分析
就在上方,基本可以确认为构造函数数组,继续跟踪
可以发现在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); }
|
运行之后结果如下
发现出现了两种运行结果,这是为什么呢?简单说下测试代码,利用反射获取了其所有构造函数,然后循环输出。
我们观察控制台的输出结果,不难发现输出的顺序竟然有所不同,这就是我所说的调用的随机性,我们再回到上面所说的对于参数数量的判断部分
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会始终调用最开始传入的那个构造函数。