FastJson反序列化和构造函数之间的一点小秘密
各位看官大家下午好,FastJson想必大家都很熟悉了,很常见的Json序列化工具。今天在下要和大家分享一波FastJson反序列化和构造函数之间的一点小秘密。
下面先进入大家都深恶痛绝的做题环节。哈哈哈...
/** * @创建人:Raiden * @Descriotion: * @Date:Created in 15:53 2020/3/21 * @Modified By: */ public class User { private String name; private String id; private String student; public User(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 + '\'' + '}'; } }
@Test public void testFastJosn() throws Throwable { String userStr = "{\n" + "\t\"name\":\"张三\",\n" + "\t\"id\":\"20200411001\",\n" + "\t\"student\":\"高三三班\"\n" + "}"; User user = JSON.parseObject(userStr, User.class); System.err.println(user); }
大家看看会打印出什么?
A:User{name='张三', id='20200411001', student='null'} B:User{name='张三', id='20200411001', student='高三三班'} C:User{name='null', id='20200411001', student='高三三班'} D:User{name='null', id='null', student='高三三班'}
没整明白吧,脑袋又嗡嗡的吧!
下面公布答案:A!
是不是有点意外啊,下面就由在下为各位解答一下疑惑。
大家都知道FastJson反序列化的时候,普通类(不包括接口和抽象类)是通过反射获取构造函数来生成对象的,最后通过反射调用set方法来设置属性的。
那为什么上面会产生这样奇怪的结果呢。想必有些聪明的看官已经猜到了吧,对没错,就是构造函数的问题。通常大家在工作中写的类都是这个样子:
@Setter @Getter public class User { private String name; private String id; private String student; }
写好类和属性以后,通过lombok生成get、set方法。构造函数在编译期间由编译器自动生成的一个无参构造函数。在FastJson反序列化的时候这样的类没有任何问题。
会依照各位看官的意思反序列化成各位所需的对象。但是,哈哈哈...只要出现转折词那下面就是重点了。
但是当我们手动为类添加有参构造函数时候,在编译器编译时,就不会再为其添加无参构造函数,也就是说你的类有且只有你添加的这个构造函数。那这样FastJson是如何反序列化生成实例的呢?
下面请各位看官移步到FastJson反序列化方法调用图和源码片段:
我们这次要讲的重点在JavaBeanInfo的build方法中。从类名中大奖可以知道,这是FastJson内部存储反序列化对象信息的类。这其中就包含了创建该类的构造方法信息。
我们先看看他的属性:
public class JavaBeanInfo { public final Class<?> clazz; public final Class<?> builderClass; //默认的构造方法放在这 public final Constructor<?> defaultConstructor; //手动写的构造方法放在这 public final Constructor<?> creatorConstructor; public final Method factoryMethod; public final Method buildMethod; public final int defaultConstructorParameterSize; public final FieldInfo[] fields; public final FieldInfo[] sortedFields; public final int parserFeatures; public final JSONType jsonType; public final String typeName; public final String typeKey; public String[] orders; public Type[] creatorConstructorParameterTypes; public String[] creatorConstructorParameters; public boolean kotlin; public Constructor<?> kotlinDefaultConstructor;
在其中我们会发现 defaultConstructor 和 creatorConstructor 两个属性。从属性名称各位看官应该能看出来其中defaultConstructor 是默认的构造函数,那我们来看看获取他的源码片段:
这段代码的含义是先遍历所有的构造函数,看看其中是否存在无参构造函数,如果存在直接返回。
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; }
接下来使用无参构造函数进行反序列化,从调试状态我们可以看到JavaBeanInfo的信息:
从调试状态的信息可以看到默认构造函数是无参构造函数,默认构造函数的参数长度为0个。
接下了请各位看官了解一下有参构造函数的获取方式:(下面的代码取自JavaBeanInfo 的403行到455行)
for (Constructor constructor : constructors) { Class<?>[] parameterTypes = constructor.getParameterTypes(); if (className.equals("org.springframework.security.web.authentication.WebAuthenticationDetails")) { if (parameterTypes.length == 2 && parameterTypes[0] == String.class && parameterTypes[1] == String.class) { creatorConstructor = constructor; creatorConstructor.setAccessible(true); paramNames = ASMUtils.lookupParameterNames(constructor); break; } } if (className.equals("org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken")) { if (parameterTypes.length == 3 && parameterTypes[0] == Object.class && parameterTypes[1] == Object.class && parameterTypes[2] == Collection.class) { creatorConstructor = constructor; creatorConstructor.setAccessible(true); paramNames = new String[] {"principal", "credentials", "authorities"}; break; } } if (className.equals("org.springframework.security.core.authority.SimpleGrantedAuthority")) { if (parameterTypes.length == 1 && parameterTypes[0] == String.class) { creatorConstructor = constructor; paramNames = new String[] {"authority"}; break; } } 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; } paramNames = lookupParameterNames; creatorConstructor = constructor; }
上面的方法的作用是从所有的构造方法中获取参数最多的一个,并将其放入JaveBeanInfo的creatorConstructor属性中,供后面实例化对象使用。
要特别注意一下,这里的获取参数名称使用的并不是java8中提供的获取方法参数名称的方式,而是通过流读取class文件的的方式来获取的。
在赋值的时候,会通过参数名称去json串中查找对应名称的字段来赋值,并且在通过构造函数赋值完毕之后,将不再通过set方法赋值(这里有坑一定要记住,否则会出现赋值不上的莫名其妙的错误)。
如果构造函数中的入参命名和JSON串中的属性名称对应不上将无法赋值,这里一定要记牢,否则会出现莫名其妙的问题。举个例子:
public User(String a,String i,String s){ this.name = a; this.id = i; this.student = s; }
上面所示的构造函数,在Json串中就必须有对应的属性a,i,s。否则会导致反序列化后属性为空。
当然这里也可以通过JSONField来从定义参数名称。想详细了解的同学可以去看看ASMUtils.lookupParameterNames(constructor)这个方法的源码。因为篇幅问题在这就不在赘述。
下面我们使用如下类进行调试,看看是否如我所说的。
public class User { private String name; private String id; private String student; public User(String name,String id){ this.name = name; this.id = id; } public User(String name,String id,String student){ this.name = name; this.id = id; this.student = student; } 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 + '\'' + '}'; } }
从调试截图中可以清晰看到,在JavaBeanInfo中creatorConstructor属性存放的是有三个参数的构造方法,而且三个参数的类型都是String。这正好印证了我们上面的结论。
从JavaBeanDeserializer类的969行到1026行源代码片段可以看到,这里直接通过反射调用有参构造函数生成了要反序列化的类。并且因为这里因为JavaBeanInfo中 buildMethod 属性为空,所以在1025行代码处直接返回结果。
至此方法结束,不在进行set赋值。
if (beanInfo.creatorConstructor != null) { boolean hasNull = false; if (beanInfo.kotlin) { for (int i = 0; i < params.length; i++) { if (params[i] == null && beanInfo.fields != null && i < beanInfo.fields.length) { FieldInfo fieldInfo = beanInfo.fields[i]; if (fieldInfo.fieldClass == String.class) { hasNull = true; } break; } } } try { if (hasNull && beanInfo.kotlinDefaultConstructor != null) { object = beanInfo.kotlinDefaultConstructor.newInstance(new Object[0]); for (int i = 0; i < params.length; i++) { final Object param = params[i]; if (param != null && beanInfo.fields != null && i < beanInfo.fields.length) { FieldInfo fieldInfo = beanInfo.fields[i]; fieldInfo.set(object, param); } } } else { //在这通过反射直接调用有参构造函数 并且输入参数进行初始化 object = beanInfo.creatorConstructor.newInstance(params); } } catch (Exception e) { throw new JSONException("create instance error, " + paramNames + ", " + beanInfo.creatorConstructor.toGenericString(), e); } if (paramNames != null) { for (Map.Entryentry : fieldValues.entrySet()) { FieldDeserializer fieldDeserializer = getFieldDeserializer(entry.getKey()); if (fieldDeserializer != null) { fieldDeserializer.setValue(object, entry.getValue()); } } } } else if (beanInfo.factoryMethod != null) { try { object = beanInfo.factoryMethod.invoke(null, params); } catch (Exception e) { throw new JSONException("create factory method error, " + beanInfo.factoryMethod.toString(), e); } } if (childContext != null) { childContext.object = object; } } //这里因为JavaBeanInfo中 buildMethod 属性为空,所以直接返回结果方法结束,不在进行set赋值 Method buildMethod = beanInfo.buildMethod; if (buildMethod == null) { return (T) object; }
到这里有参构造函数的流程基本也就结束了。
下面是反序列化流程的逻辑图:
在最后特别介绍下com.alibaba.fastjson.util.FieldInfo 这个类。Fastjson就是通过这个类给无参构造函生成的实例赋值的。
public class FieldInfo implements Comparable{ public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException { return method != null ? method.invoke(javaObject) : field.get(javaObject); } public void set(Object javaObject, Object value) throws IllegalAccessException, InvocationTargetException { if (method != null) { method.invoke(javaObject, new Object[] { value }); return; } field.set(javaObject, value); } }
从源代码片段中可以看出,不管是赋值还是获取值,都是先通过反射调用get,set方法来实现的,但是如果没有get,set方法会通过反射调用field来实现。也就是说没有get,set也是可以序列化和反序列化的。
到这里本篇分享就要结束了,不知道各位看官大大是否满意。满意的话希望给个小小的赞以示鼓励,感谢大家的收看。