各位看官大家下午好,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.Entry<String, Object> entry : 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<FieldInfo> {

    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也是可以序列化和反序列化的。

到这里本篇分享就要结束了,不知道各位看官大大是否满意。满意的话希望给个小小的赞以示鼓励,感谢大家的收看。

FastJson反序列化和构造函数之间的一点小秘密的更多相关文章

  1. fastjson反序列化多层嵌套泛型类与java中的Type类型

    在使用springmvc时,我们通常会定义类似这样的通用类与前端进行交互,以便于前端可以做一些统一的处理: public class Result<T> { private int ret ...

  2. Fastjson反序列化漏洞分析 1.2.22-1.2.24

    Fastjson反序列化漏洞分析 1.2.22-1.2.24 Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两 ...

  3. .NET高级代码审计(第三课)Fastjson反序列化漏洞

    0X00 前言 Java中的Fastjson曾经爆出了多个反序列化漏洞和Bypass版本,而在.Net领域也有一个Fastjson的库,作者官宣这是一个读写Json效率最高的的.Net 组件,使用内置 ...

  4. Fastjson反序列化漏洞基础

    Fastjson反序列化漏洞基础 FastJson是alibaba的一款开源JSON解析库,可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象. 0x0 ...

  5. fastjson反序列化-JdbcRowSetImpl利用链

    fastjson反序列化-JdbcRowSetImpl利用链 JdbcRowSetImpl利用链 fastjson反序列化JdbcRowSetImpl - Afant1 - 博客园 (cnblogs. ...

  6. JS 之原型,实例,构造函数之间的关系

    JS是面向对象的语言,函数也是对象.下面大致介绍下实例,原型与构造函数之间的关系. 构造函数模式 function Person(name,age){ this.name = name; this.a ...

  7. FastJson反序列化漏洞利用的三个细节 - TemplatesImpl的利用链

    0. 前言 记录在FastJson反序列化RCE漏洞分析和利用时的一些细节问题. 1. TemplatesImpl的利用链 关于 parse 和 parseObject FastJson中的 pars ...

  8. Fastjson反序列化漏洞概述

    Fastjson反序列化漏洞概述 ​ 背景 在推动Fastjson组件升级的过程中遇到一些问题,为帮助业务同学理解漏洞危害,下文将从整体上对其漏洞原理及利用方式做归纳总结,主要是一些概述性和原理上的东 ...

  9. fastjson反序列化漏洞研究(上)

    前言 最近护网期间,又听说fastjson传出“0day”,但网上并没有预警,在github上fastjson库中也有人提问关于fastjson反序列化漏洞的详情.也有人说是可能出现了新的绕过方式.不 ...

随机推荐

  1. SQLi-Labs之1~6关 - 常规注入与盲注

    年初五 财神入 第一关 联合注入 1.准备 2.加'截断 3.order by 判断查询列数 4.同上 5.联合查询判断字段位置 6.查数据库名 7.1 查表名 7.2 查列名 8.查数据 第二关 不 ...

  2. 【图文+视频新手也友好】Java一维数组详细讲解(内含练习题答案+详解彩蛋喔~)

    目录 视频讲解: 一.数组的概述 二.一维数组的使用 三.Arrays工具类中的sort方法(sort方法用的多,我们具体讲一下) 四.数组中的常见异常 五.一维数组练习题 六.彩蛋(本期视频使用的P ...

  3. ApplicationContextInitializer的理解和使用

    一.ApplicationContextInitializer 介绍 1.1 作用 ApplicationContextInitializer 接口用于在 Spring 容器刷新之前执行的一个回调函数 ...

  4. 使用synchronized修饰静态方法和非静态方法有什么区别

    前言 最近被问到了这个问题,第一次回答的也是很不好,在此参考网上答案进行整理记录.供大家学习参考. Synchronized修饰非静态方法 Synchronized修饰非静态方法,实际上是对调用该方法 ...

  5. golang Printf 函数有超过 10 个转义字符

    verb 描述 %d 十进制整数 %x, %o, %b 十六进制.八进制.二进制整数 %f, %g, %e 浮点数:如 3.141593, 3.141592653589793, 3.141593e+0 ...

  6. 基于 HTML + WebGL 结合 23D 的疫情地图实时大屏 PC 版【转载】

    前言 2019年12月以来,湖北省武汉市陆续发现了多例肺炎病例,现已证实为一种新型冠状病毒感染引起的急性呼吸道传染病并蔓延全国,肺炎疫情牵动人心,人们每天起来第一件事变成了关注疫情进展,期望这场天灾早 ...

  7. 使用C#+EmguCV处理图像入门(一)

    首先我们先了解一下该库的一些相关信息 OpenCV(Open Source Computer Vision Library)是一个(开源免费)发行的跨平台计算机视觉库,可以运行在Linux.Windo ...

  8. mysql数据库设计文档-导出字段设计

    navicat 是我一直在使用的一个数据库操作工具,非常方便快捷.如果没有可用navicat可以留言邮箱我直接发您. 今天来介绍一下使用navicat导出数据库字段设计.废话不多说,先看导出效果. 查 ...

  9. Mol Cell Proteomics. | Proteomics Analysis of Extracellular Matrix Remodeling During Zebrafish Heart Regeneration (解读人:徐宁)

    文献名:Proteomics Analysis of Extracellular Matrix Remodeling During Zebrafish Heart Regeneration(斑马鱼心脏 ...

  10. Unity 游戏框架:资源管理神器 ResKit

    此篇文章准备了将近两周的时间,写了改,改了删.之前有朋友反馈,上一个文章太冗长了,影响阅读体验,这一讲就走个精简路线.所以只要不是很重要的内容就都删减掉了. 文章分两个部分,第一部分是原理,第二部分是 ...