经验专题-序列化与反序列化-1、选型比较
1. Java 序列化
https://cloud.tencent.com/developer/article/1752784
1.1. 是什么
如今大部分的后端服务都是基于微服务架构实现的,服务按照业务划分被拆分,实现了服务的解耦,同时也带来了一些新的问题,比如不同业务之间的通信需要通过接口实现调用。两个服务之间要共享一个数据对象,就需要从对象转换成二进制流,通过网络传输,传送到对方服务,再转换成对象,供服务方法调用。这个编码和解码的过程我们称之为序列化和反序列化。
在高并发系统中,序列化的速度快慢,会影响请求的响应时间,序列化后的传输数据体积大,会导致网络吞吐量下降,所以,一个优秀的序列化框架可以提高系统的整体性能。
我们都知道 Java 提供了 RMI 框架可以实现服务与服务之间的接口暴露和调用,RMI 中对数据对象的序列化采用的是 Java 序列化。而目前主流的框架却很少使用到 Java 序列化,如 SpringCloud 使用的 Json 序列化,Dubbo 虽然兼容了 Java 序列化,但是默认还是使用的 Hessian 序列化。
Java 序列化
首先,来看看什么是 Java 序列化和实现原理。Java 提供了一种序列化机制,这种机制能将一个对象序列化成二进制形式,用于写入磁盘或输出到网络,同时将从网络或者磁盘中读取的字节数组,反序列化成对象,在程序中使用。
关键字和接口-1、Serializable接口1.2. 特性
- JDK 提供的两个输入、输出流对象 ObjectInputStream 和 ObjectOutputStream,它们只能对实现了 Serializable 接口的类的对象进行反序列化和序列化。
- ObjectOutputStream 的默认序列化方式,仅对对象的非 transient 的实例变量进行序列化,而不会序列化对象的 transient 的实例变量,也不会序列化静态变量。
- 在实现了 Serializable 接口的类的对象中,会生成一个 serialVersionUID 的版本号,这个版本号有什么用呢?它会在反序列化过程中来验证序列化对象是否加载了反序列化的类,如果是具有相同类名的不同版本号的类,在反序列化中是无法获取对象的。
- 具体实现序列化的是 writeObject 和 readObject,通常这两个方法是默认的,我们也可以在实现 Serializable 接口的类中对其重写,定制属于自己的序列化和反序列化机制。
- Java 序列化类中还定义了两个重写方法:writeReplace() 和 readResolve(),前者是用来在序列化之前替换序列化对象的,后者是用来在序列化之后对返回对象进行处理的。
1.3. 缺陷
❕ ^st48nr
1.3.1. 无法跨语言
而 Java 序列化目前只支持 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议,因此,如果两个基于不同语言编写的应用程序之间通信,使用 Java 序列化,则无法实现两个应用服务之间传输对象的序列化和反序列化。
1.3.2. 容易被攻击
我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。
之前 FoxGlove Security 安全团队的一篇论文中提到的:通过 Apache Commons Collections,Java 反序列化漏洞可以实现攻击,一度横扫了 WebLogic、WebSphere、JBoss、Jenkins、OpenNMS 的最新版,各大 Java Web Server 纷纷躺枪。
其实,Apache Commons Collections 就是一个第三方基础库,它扩展了 Java 标准库里的 Collection 结构,提供了很多强大的数据结构类型,并且实现了各种集合工具类。
实现攻击的原理:Apache Commons Collections 允许链式的任意的类函数反射调用,攻击者通过实现了 Java 序列化协议的端口,把攻击代码上传到服务器上,再由 Apache Commons Collections 里的 TransformedMap 来执行。
如何解决这个漏洞?
很多序列化协议都制定了一套数据结构来保存和获取对象。例如,JSON 序列化、ProtocolBuf 等,它们只支持一些基本类型和数组数据类型,这样可以避免反序列化创建一些不确定的实例。虽然它们的设计简单,但足以满足当前大部分系统的数据传输需求。我们也可以通过反序列化对象白名单来控制反序列化对象,可以重写 resolveClass 方法,并在该方法中校验对象名字。代码如下所示:
1 |
|
1.3.3. 序列化流大
序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。
1.3.4. 性能太差
上边说了 4 个 Java 序列化的缺点,其实业界有很多可以代替 Java 序列化的序列化框架,大部分都避免了 Java 默认序列化的一些缺陷,例如比较流行的 FastJson、Kryo、Protobuf、Hessian 等,这里就来简单的介绍一下 Protobuf 序列化框架。
2. Protobuf
Protobuf 是由 Google 推出且支持多语言的序列化框架,目前在主流网站上的序列化框架性能对比测试报告中,Protobuf 无论是编解码耗时,还是二进制流压缩大小,都名列前茅。
Protobuf 以一个 .proto 后缀的文件为基础,这个文件描述了字段以及字段类型,通过工具可以生成不同语言的数据结构文件。在序列化该数据对象的时候,Protobuf 通过.proto 文件描述来生成 Protocol Buffers 格式的编码。
3. 选型
Java 默认的序列化是通过 Serializable 接口实现的,只要类实现了该接口,同时生成一个默认的版本号,我们无需手动设置,该类就会自动实现序列化与反序列化。
Java 默认的序列化虽然实现方便,但却存在安全漏洞、不跨语言以及性能差等缺陷,所以我强烈建议你避免使用 Java 序列化。
纵观主流序列化框架,FastJson、Protobuf、Kryo 是比较有特点的,而且性能以及安全方面都得到了业界的认可,我们可以结合自身业务来选择一种适合的序列化框架,来优化系统的序列化性能
3.1. SpringBoot 集成 ES
在集成 SpringBoot 与 ElasticSearch 时,关于 LocalDateTime 类型的序列化与反序列化报错,SpringBoot 默认的 Jackson 用起来不是很顺手(需要实例化 ObjectMapper),便计划使用 FastJson,然而,直接引入 FastJson 后,会与默认的 Jackson 发生冲突。。
原文链接: https://blog.csdn.net/u013810234/article/details/106975976
4. 实战经验
5. 参考与感谢
https://cloud.tencent.com/developer/article/1752784
https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html
https://www.cnblogs.com/mic112/p/15559723.html
https://developer.afengblog.com/rear-end/java/798.html
https://juejin.cn/post/6974565210161954829
https://developer.aliyun.com/article/1136866