序列化
目录
¶概述
❓什么是序列化与反序列化
📓通常将序列化和反序列这两个操作,简称为序列化,具体的指代根据上下文确定可以确认
❓为什么需要序列化与反序列化?
在高并发系统中,序列化的速度快慢,会影响请求的响应时间,序列化后的传输数据体积大,会导致网络吞吐量下降,所以,一个优秀的序列化框架可以提高系统的整体性能,但是 Java 的序列化性能实在不怎么好
❓Java 是如何实现序列化和反序列化的?
🎶在序列化的整个过程中都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象
¶Java 序列化细节
一个 Java 对象要能序列化,必须实现一个特殊的java.io.Serializable
接口,Serializable
接口没有定义任何方法,它是一个空接口,这样的空接口称为标记接口(Marker Interface),实现了标记接口的类相当于给自身贴了个标记,本质上并没有增加任何代码逻辑
也可以实现
Externalizable
接口,用于自定义序列化字段,但是由于Externalizable
增加了编码的复杂度,所以推荐使用Serializable
接口
¶序列化
把一个 Java 对象变为 byte[] 数组,需要使用 ObjectOutputStream
,它负责把一个 Java 对象写入一个字节流:
1 | public class Main { |
ObjectOutputStream
既可以写入基本类型,如 int
,boolean
,也可以写入String
(以 UTF-8 编码),还可以写入实现了 Serializable 接口的 Object,因为写入 Object 时需要大量的类型信息,所以写入的内容很大
¶反序列化
和 ObjectOutputStream 相反,ObjectInputStream
负责从一个字节流读取 Java 对象:
1 | try (ObjectInputStream input = new ObjectInputStream(...)) { |
除了能读取基本类型和 String 类型外,调用 readObject()
可以直接返回一个 Object 对象,要把它变成一个特定类型,必须强制转型
readObject()可能抛出的异常有:
为了避免 class 定义变动导致的不兼容,Java 的序列化允许 class 定义一个特殊的serialVersionUID
静态变量,用于标识 Java 类的序列化“版本”,通常可以由 IDE 自动生成。如果增加或修改了字段,可以改变serialVersionUID
的值,这样就能自动阻止不匹配的 class 版本:
1 | public class Person implements Serializable { |
🎶反序列化的几个重要特点:反序列化时,由 JVM 直接构造出 Java 对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行
¶transient 关键字
❓为什么要用transient
关键字
🎶static 修饰的静态变量天然就是不可序列化的
✨transient 的作用
¶序列化的安全性
虽然 Java 提供了 RMI 框架可以实现服务与服务之间的接口暴露和调用,RMI 中对数据对象的序列化采用的是 Java 序列化。而目前主流的框架却很少使用到 Java 序列化,如 SpringCloud 使用的 Json 序列化,Dubbo 虽然兼容了 Java 序列化,但是默认还是使用的 Hessian 序列化。
实际上,Java 本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过 JSON 这样的通用数据结构来实现,只输出基本类型(包括 String )的内容,而不存储任何与代码相关的信息。
常见的 RPC 通信框架中,很少会发现使用 JDK 提供的序列化,主要是因为 JDK 默认的序列化存在着如下一些缺陷:无法跨语言、易被攻击、序列化后的流太大、序列化性能太差等
¶无法跨语言
现在很多系统的复杂度很高,采用多种语言来编码,而 Java 序列化目前只支持 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 Java 序列化这套协议,因此,如果两个基于不同语言编写的应用程序之间通信,使用 Java 序列化,则无法实现两个应用服务之间传输对象的序列化和反序列化
¶易被攻击
对于不信任数据的反序列化,从本质上来说是危险的,应该避免
对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。
例如下面这个案例就可以很好地说明
1 | Set root = new HashSet(); |
FoxGlove Security 安全团队的一篇论文中提到的:通过 Apache Commons Collections,Java 反序列化漏洞可以实现攻击。Apache Commons Collections 就是一个第三方基础库,它扩展了 Java 标准库里的 Collection 结构,提供了很多强大的数据结构类型,并且实现了各种集合工具类
¶实现攻击的原理
Apache Commons Collections 允许链式的任意的类函数反射调用,攻击者通过实现了 Java 序列化协议的端口,把攻击代码上传到服务器上,再由 Apache Commons Collections 里的 TransformedMap 来执行
¶如何解决这个漏洞?
很多序列化协议都制定了一套数据结构来保存和获取对象。例如,JSON 序列化、ProtocolBuf 等,它们只支持一些基本类型和数组数据类型,这样可以避免反序列化创建一些不确定的实例。虽然它们的设计简单,但足以满足当前大部分系统的数据传输需求。我们也可以通过反序列化对象白名单来控制反序列化对象,可以重写 resolveClass 方法,并在该方法中校验对象名字
¶序列化后的流太大
序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量
¶序列化性能太差
序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间
¶各种序列化技术
序列化只是一种思想,将序列化转换为不同的二进制流就会产生不同的技术,常见的技术有:XML 序列化框架、JSON 序列化框架、Hessian 序列化框架、Avro 序列化、kyro 序列化框架 和 Protobuf 序列化框架
¶XML 序列化框架
👍优点:可读性好,方便阅读和调试
😣缺点:序列化以后字节码比较大;效率不高
✨使用场景
📚实现方式
¶JSON 序列化框架
👍优点:传输的字节流更小,可读性非常好
📚实现方式
🆚常用框架对比
¶Hessian 序列化框架
✨Hessian 序列化特点
- 跨语言传输的二进制序列化协议
- 比 Java 默认的序列化机制有着更好的性能和易用性
📚Dubbo 就是使用重构后Hessian
序列化机制,达到更高的性能
¶Avro 序列化
✨Avro 序列化特点
- 支持大批量数据交换的应用
- 支持二进制序列化方式
- 可以便捷,快速地处理大量数据
- 动态语言优化
¶kyro 序列化框架
✨Kryo 是一种非常成熟的序列化实现,已经在 Hive、 Storm 中使用得比较广泛,不过它不能跨语言
¶Protobuf 序列化框架
Protobuf 是由 Google 推出且支持多语言的序列化框架,目前在主流网站上的序列化框架性能对比测试报告中,Protobuf 无论是编解码耗时,还是二进制流压缩大小,都名列前茅
✨Protobuf 序列化框架特点
- Protobuf 是 Google 的一种数据交换格式,它独立于语言、独立于平台
- Protobuf 是一个纯粹的表示层协议,可以和各种传输层协议一起使用
- Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用
- 序列化以后数据量相对较少,也可以应用在对象的持久化场景中
😣protobuf 有个缺点就是要传输的每一个类的结构都要生成对应的 proto 文件,如果某个类发生修改,还得重新生成该类对应的 proto 文件
¶序列化的技术选型
👴选型建议
- 对性能要求不高的场景,可以采用基于 XML 的 SOAP 协议
- 对性能和间接性有比较高要求的场景,那么 Hessian、 Protobuf、 Thrift、 Avro 都可以
- 基于前后端分离,或者独立的对外的 api 服务,选用 JSON 是比较好的,对于调试、可读性都很不错
- 动态类型语言使用 Avro