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.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反序列化和构造函数之间的一点小秘密的更多相关文章
- fastjson反序列化多层嵌套泛型类与java中的Type类型
在使用springmvc时,我们通常会定义类似这样的通用类与前端进行交互,以便于前端可以做一些统一的处理: public class Result<T> { private int ret ...
- Fastjson反序列化漏洞分析 1.2.22-1.2.24
Fastjson反序列化漏洞分析 1.2.22-1.2.24 Fastjson是Alibaba开发的Java语言编写的高性能JSON库,用于将数据在JSON和Java Object之间互相转换,提供两 ...
- .NET高级代码审计(第三课)Fastjson反序列化漏洞
0X00 前言 Java中的Fastjson曾经爆出了多个反序列化漏洞和Bypass版本,而在.Net领域也有一个Fastjson的库,作者官宣这是一个读写Json效率最高的的.Net 组件,使用内置 ...
- Fastjson反序列化漏洞基础
Fastjson反序列化漏洞基础 FastJson是alibaba的一款开源JSON解析库,可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象. 0x0 ...
- fastjson反序列化-JdbcRowSetImpl利用链
fastjson反序列化-JdbcRowSetImpl利用链 JdbcRowSetImpl利用链 fastjson反序列化JdbcRowSetImpl - Afant1 - 博客园 (cnblogs. ...
- JS 之原型,实例,构造函数之间的关系
JS是面向对象的语言,函数也是对象.下面大致介绍下实例,原型与构造函数之间的关系. 构造函数模式 function Person(name,age){ this.name = name; this.a ...
- FastJson反序列化漏洞利用的三个细节 - TemplatesImpl的利用链
0. 前言 记录在FastJson反序列化RCE漏洞分析和利用时的一些细节问题. 1. TemplatesImpl的利用链 关于 parse 和 parseObject FastJson中的 pars ...
- Fastjson反序列化漏洞概述
Fastjson反序列化漏洞概述 背景 在推动Fastjson组件升级的过程中遇到一些问题,为帮助业务同学理解漏洞危害,下文将从整体上对其漏洞原理及利用方式做归纳总结,主要是一些概述性和原理上的东 ...
- fastjson反序列化漏洞研究(上)
前言 最近护网期间,又听说fastjson传出“0day”,但网上并没有预警,在github上fastjson库中也有人提问关于fastjson反序列化漏洞的详情.也有人说是可能出现了新的绕过方式.不 ...
随机推荐
- niginx:duplicate MIME type "text/html" in nginx.conf 错误(转载)
把nginx升级到最新以后,发现用原来的配置启动的时候会提示: duplicate MIME type "text/html" in /usr/local/nginx/conf/n ...
- 基于Python3 + appium的Ui自动化测试框架
UiAutoTest 一.概要 数据驱动的Ui自动化框架 二.环境要求 框架基于Python3 + unittest + appium 运行电脑需配置adb.aapt的环境变量,build_tools ...
- Natas18 Writeup(Session登录,暴力破解)
Natas18: 一个登录界面,查看源码,发现没有连接数据库,使用Session登录,且$maxid设定了不大的上限,选择采取爆破. 源码解析: <html> <head> & ...
- 使用tomcat运行时提示some characters cannot be mapped using iso-8859-1 character encoding异常
今天第一次使用java进行jsp项目搭建,也是第一次使用tomcat.tomcat是运行java web的一个小型服务器,属于Apache的一个开源免费的服务. 在运行web 的时候,我们就要先配置好 ...
- Vue 使用百度地图 实现搜索 定位
要求能定位到国外 及 查看了文档 百度支持东南亚大部分地区 满足需求 从而使用百度地图 <template> <div class="addHospital"& ...
- The import org.springframework cannot be resolved
刚开始学spring框架时import org.springframework.context.support.ClassPathXmlApplicationContext;报错 我建的是maven项 ...
- demo08-js条件运算符
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- MySql最左匹配原则解析
看前提问:table中有多个字段组成的联合索引(a,b,c),查询时哪些情况能够命中索引呢? 话不多说,直接开搞: 数据库表结构如下: CREATE TABLE `test` ( `id` ) uns ...
- Diagnostics: File file:/private/tmp/spark-d4ebd819-e623-47c3-b008-2a4df8019758/__spark_libs__6824092999244734377.zip does not exist java.io.FileNotFoundException: File file:/private/tmp/spark-d4ebd819
spark伪分布式模式 on-yarn出现一下错误 Diagnostics: File file:/private/tmp/spark-d4ebd819-e623-47c3-b008-2a4df801 ...
- JSP+Servlet+C3P0+Mysql实现的简单新闻系统
项目简介 项目来源于:https://gitee.com/glotion/servlet-jsp_news 本系统基于JSP+Servlet+C3P0+Mysql.涉及技术少,易于理解,适合JavaW ...