参看:
- 《深入理解序列化与反序列化》牛晓丽
- https://www.bilibili.com/video/BV1E7411q7QK
- https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
一、概念&作用
将本地已经实例化的某个对象(此对象有参数),有 传输或持久化、重构Java对象 的需求,怎么才能把数据包装到一起?
众所周知,参数指向内存中的地址,这些内存地址存储着实际的数据,这些地址可能并不连续,这就使得在内存中到处都有此对象的参数数据,怎么才能把这些数据全部集合在一起呢?得到集成在一起的数据了,怎么才能把它转化为内存的对象参数呢?
这就需要使用 序列化和反序列化 技术:
- 序列化:将某个对象集成压缩成 字节流或文本 的形式,将参数引用指向的数据全部集成在一起(参数对象可能有多个引用,指向内存中不同的地方,集成就是把全部数据整理到内存中一块连续的区域)。
- 反序列化:将 字节流或文本 转换成内存中的对象实例,即在内存中分配对象的内存并赋值(参数地址和引用)。
注意:即使是经过序列化之后传递的数据,也是经过 计算机网络的五层架构 进行数据传输,最终都是以为二进制形式传输的。
序列化常用类型分为 text and binary 两种形式(不常用不列),即 字符串和字节流。
- text : JSON、XML等都是序列化后的数据。比较成熟的开源类库有Gson、Fastjson。
- binary:字节流,更快更轻盈(String字符串其就是)。
从Java9开始,String使用byte[]存储,即字节流。
序列化技术,具有 跨语言、跨平台、可扩展和向下兼容 的优点。比如C语言中序列化一个对象后,可以被对应的Java语言解析出来。
二、Java实现序列化
数据对象,必须被encoding编码以后才能被序列化(UTF-8等)。下面使用Java分别实现序列化实体类实例为 字节流和文本,文本处以JSON数据格式为例。
1.字节流序列化
Java实现序列化注意的点:
- 某个类可以被序列化,则其子类也可以被序列化
- 声明为 static 和 transient 的成员变量,不能被序列化。static 成员变量是描述类级别的属性,transient 表示临时数据。
- 反序列化读取序列化对象的顺序要保持一致。
- 只有实现了 Serializable及其子类 的对象才能被序列化,否则抛出异常。(好像,json的序列化不用实现)。
以序列化为二进制数据存储到本地为例(字节流序列化实际应用偏向于存储到本地),简单实现:
实体类User
public class User implements Serializable {
// getter,setter,constructor 加上
private static final long serialVersionUID = 5887391604554532906L;
private String name;
private Integer age;
private Character sex;
}
序列化和反序列化操作
public static void main(String[] args) throws Exception {
// 序列化 二进制存储到本地
User user = new User("张三",22,'男');
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("User.obj"));
oos.writeObject(user); // 写到本地文件中
//反序列化 从本地文件中获取数据
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("User.obj"));
User user1 = (User) ois.readObject();
System.out.println(user1);
}
现在,就可在当前项目的根目录看到此文件(也可以自己指定位置),打开它(建议使用sublime text或其他编辑器打开):
2.JSON序列化
以为JSON序列化,进行打印为例。JSON序列化实际应用偏向于传输数据。这里使用Java类库实现简单Java对象转换为JSON字符串:
// user 是前面代码中的实现
// 使用java类库 throws JsonProcessingException
String json = new ObjectMapper().writeValueAsString(user);
System.out.println(json);
输出如下
{“name”:“张三”,“age”:22,“sex”:“男”}
简单实现肯定是有一些问题,那么为什么我这么敷衍?因为我们可以使用阿里的fastjson类库,使用及其广泛,特别强大!【点击我查看并学习fastjson使用】。
3.二者区别
- 二进制序列化器需要 实体类实现Serializable接口或其子类,而Json序列化器不需要。
- 二进制序列化器只能序列化字段,而不能序列化属性,也就是说当一个类中不显示定义字段,二进制序列化器是不起作用的,JSON序列化器没有这条规定。二进制序列化器中,若某个字段不需要序列化,可应用[NonSerialized]特性。
- 二进制序列化后得到的是一个二进制文件,而JSON序列化后得到的是JSON字符串。
- 二进制序列化JSON所在内存小,JSON易读。json依据逐渐演变成前后端交互数据的定义格式。
三、序列化安全
1.serialVersionUID
针对于二进制序列化
举个例子:在平时我们与人说话交流的时候,大家都用普通话,如果有人使用家乡话,你就会感觉到听不懂了,更别说好好交流了。这里的普通话就是一种协议,作为我们与身边人的语言协议,大家都遵守它才能高效沟通。
而serialVersionUID就是序列化操作的协议,我在解析别人传递给我的数据的时候,会去检查此文件是否遵守协议,遵守我才能正常解析,不遵守我就直接抛出异常(解析也会出现问题)。即反序列化时,JVM会判断serialVersionUID是否一致,不一致就会直接抛出异常。
比如前后使用不同的serialVersionUID:
当我们一个实体类中没有显式的定义一个名为 serialVersionUID、类型为long的变量时,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID。时间不同,也会出错。那么如何决呢?便是在本地类中加个serialVersionUID变量,值保持不变,便可以进行序列化和反序列化(显式的定义一个serialVersionUID)。
2.反序列化不安全
下面这些地方同样可能存在反序列化安全隐患,但很可能不常被关注到:
- 自定义的HTTP Header。
- 存储在Redis、MongoDB、MySQL等数据库里的数据,可能被不怀好意的员工修改。
- 存储在服务器本地,或者某个远程文件服务器里的文件,可能被攻击者替换。
- 缓存服务器中的数据可能被攻击者污染。
治病需要除根,能从根本上阻止反序列化安全问题的防御方案就是完整性校验,而最常见的例子之一就是JWT。
【更多序列化的问题请移步:https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html】。