各位看官大家下午好,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. arm 添加 samb 文件共享

    编译环境: ubunto 12 arm-linux-gcc 4.3.2 arm linux 4.1.36 开发板 2440 测试上传速度,大文件 github源码 https://github.com ...

  2. 原生js实现随着滚动条滚动,导航会自动切换的效果

    最近学习前端,把前面用原生js写的一段有关tab切换效果的代码贴上,实现的效果比较简陋,请大家见谅 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1 ...

  3. 解决Ajax中IE浏览器缓存问题

    解决Ajax中IE浏览器缓存问题 1.首先,先看一张图.从这张图中我们可以清楚的了解到从请求的发出到解析响应的过程. 2.根据图中的三个节点我们可以使用三种方式解决这个缓存问题(主要是针对ie) 2. ...

  4. 部署nginx后无法访问数据库,查看www-error.log日志报错Class 'mysqli' not found in /usr/local/nginx/html/mysql.php on line 2

    检查你的php-mysql包是否安装 [root@localhost nginx]# rpm -qa php-mysql 没有任何输出则没有安装,接下来用yum安装php-mysql yum -y i ...

  5. IPv6 时代如何防御 DDoS 攻击?

    在互联网世界,每台联网的设备都被分配了一个用于标识和位置定义的 IP 地址.20 世纪 90 年代以来互联网的快速发展,联网设备所需的地址远远多于可用 IPv4 地址的数量,导致了 IPv4 地址耗尽 ...

  6. C/C++、C#、JAVA(一):代码模板与库代码的引入

    代码默认模板 编译性高级编程语言中,几乎每种语言,都有个静态的 main 方法作为程序启动入口,每种语言都有其编写规范.为了学习 C/C++.C#.JAVA四种语言,我们要先从默认代码模板中,慢慢摸索 ...

  7. Mybatis(一)Mybatis相关概念

    1.1 传统的JDBC实现 public static void main(String[] args) { Connection connetion = null; PreparedStatemen ...

  8. 配置GitLab或Git环境之教程

    配置GitLab或Git环境之教程 1.安装好Git后,首先打开开始菜单的所有程序里面的git文件夹,打开Git Bash/ ​ 2.弹出的命令行里面输入ssh-keygen 输入y,一直Enter ...

  9. 2019HECTF总结_web题

    奇怪的编码 ♭|§∮♯♭|§∮♬♭|§§♫♭|§∮§♭|§♩§♭|♯♬¶♭|§§♫♭|§§¶♭|♯¶§♭|♯¶♫♭|§∮♭♭|§§♫♭|§§♬♭|♯♬♪♭|♯¶♪♭|♯¶|♭|♯¶♯♭|♯♬♬♭|♯♬ ...

  10. [剑指Offer]41.和为S的两个数字 VS 和为S的连续正数序列

    [剑指Offer]41 和为S的两个数字 VS 和为S的连续正数序列 Leetcode T1 Two Sum Given an array of integers, return indices of ...