Java序列化详解
目录
一、什么是序列化
二、什么是反序列化
三、序列化和反序列化的作用
四、序列化和反序列化应用案例
五、常见序列化协议对比
5.1 JDK 自带的序列化方式
5.2 JDK序列化的缺陷
1. 无法跨语言
2. 易被攻击
3. 序列化后的流太大
4. 序列化性能太差
5.3 Kryo
5.4 Protobuf
5.5 总结
一、什么是序列化
序列化是指将对象转化为字节流的过程,以便于存储或传输。在序列化过程中,对象的状态被保存为一连串的字节,可以将这些字节保存到文件中或通过网络传输。序列化后的字节流可以在需要时进行反序列化,将字节流重新转化为对象,并恢复对象的状态。
在Java中,对象的序列化是通过实现Serializable接口来实现的。Serializable接口是一个标记接口,没有任何方法,只是用于标识一个类可以被序列化。当一个类实现了Serializable接口,它的对象就可以被序列化为字节流。
序列化在很多场景中都有应用,例如在分布式系统中,可以将对象序列化后通过网络传输,或者将对象存储到缓存中。但需要注意的是,序列化和反序列化可能会引发安全问题,因此在进行序列化和反序列化操作时,要谨慎处理。
二、什么是反序列化
反序列化是指将字节流转化为对象的过程,与序列化相反。在反序列化过程中,字节流被重新组装成对象,并恢复对象的状态。在JDK中,反序列化是通过ObjectInputStream类来实现的。ObjectInputStream类提供了readObject()方法,用于从输入流中读取对象。
反序列化是序列化的逆过程,可以将序列化后的字节流重新转化为原始的对象。这在很多场景中都有应用,例如在分布式系统中,可以将序列化后的对象通过网络传输,然后在接收方进行反序列化,恢复原始的对象。需要注意的是,在进行反序列化操作时,需要确保序列化和反序列化的版本一致,否则可能会出现不兼容的问题。
三、序列化和反序列化的作用
序列化和反序列化是用于在Java中将对象转换为字节流并从字节流中恢复对象的过程。它们的作用主要有以下几个方面:
数据持久化:通过序列化,可以将对象转换为字节流并保存到文件系统或数据库中。这样可以实现数据的持久化存储,方便后续的读取和使用。
对象传输:通过序列化,可以将对象转换为字节流,并在网络中传输。这在分布式系统、远程方法调用等场景中非常常见。发送方将对象序列化为字节流,通过网络发送给接收方,接收方再通过反序列化将字节流转换为对象。
缓存机制:序列化可以用于缓存机制,将对象序列化后存储在缓存中,以提高系统性能和响应速度。当需要使用对象时,可以直接从缓存中反序列化获取对象,避免了频繁的数据库访问或计算操作。
跨平台和跨语言通信:通过序列化,可以将对象转换为字节流,使得对象在不同的平台和不同的编程语言之间进行通信成为可能。只要各方都能正确地进行序列化和反序列化操作,就可以实现跨平台和跨语言的通信。
序列化和反序列化提供了一种方便的方式来将对象转换为字节流,并在需要时恢复为原始对象。它们在数据持久化、对象传输、缓存机制以及跨平台和跨语言通信等方面都有广泛的应用。
四、序列化和反序列化应用案例
序列化和反序列化在实际应用中有很多用途,以下是一些常见的应用案例:
数据库缓存:在Web应用中,为了提高性能,我们通常会将一些经常使用的数据缓存到内存中,以减少数据库的访问次数。这时,我们可以将数据对象序列化为字节流,存储到缓存中。当需要使用数据时,我们可以从缓存中读取字节流,并反序列化为对象,以提高访问速度。
分布式系统通信:在分布式系统中,不同的节点之间需要进行通信,传递数据对象是非常常见的操作。这时,我们可以将数据对象序列化为字节流,并通过网络传递。接收方再将字节流反序列化为对象,以获取数据。
消息队列:消息队列是一种常用的异步通信方式,可以将消息对象序列化为字节流,放入消息队列中。消费者再从消息队列中获取字节流,并反序列化为对象,以获取消息内容。
远程方法调用:在分布式系统中,我们通常需要调用远程节点的方法。这时,我们可以将方法参数对象序列化为字节流,通过网络传递给远程节点,并在远程节点上反序列化为对象,以调用方法。
对象持久化:在一些应用中,我们需要将数据对象持久化到磁盘中,以便下次启动应用时能够恢复数据。这时,我们可以将数据对象序列化为字节流,存储到磁盘中。下次启动应用时,我们可以从磁盘中读取字节流,并反序列化为对象,以恢复数据。
序列化和反序列化在很多应用场景中都有广泛的应用,可以方便地将对象转换为字节流,并在需要时恢复为原始对象。
五、常见序列化协议对比
JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且部分版本有安全漏洞。(为啥效率低?安全漏洞又是啥?)比较常用的序列化协议有 hessian、kyro、protostuff。
下面提到的都是基于二进制的序列化协议,像 JSON 和 XML 这种属于文本类序列化方式。虽然 JSON 和 XML 可读性比较好,但是性能较差,一般不会选择。
5.1 JDK 自带的序列化方式
JDK 自带的序列化,只需实现 java.io.Serializable
接口即可。比如下面的Student类:
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@ToString
public class Student implements Serializable {
private static final long serialVersionUID = 1905122041950251207L;
private String name;
private transient Integer age;
private String address;
}
private transient Integer age
中的transient
表示不会序列化该属性,当对象被序列化时该属性age不会被序列化,反序列化时,该属性是以默认值赋值。比如下面反序列化时,age的值为空。
public class SerializeOperation {
/**
* 序列化对象,并使用了try-with-resources
* @param student
* @param path
* @throws IOException
*/
public static void serializeToFile(Student student, String path) throws IOException {
Student s = new Student("小明",16,"翻斗花园");
try(FileOutputStream fileOut = new FileOutputStream(path);
ObjectOutputStream out = new ObjectOutputStream(fileOut);) {
out.writeObject(s);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 反序列化对象
* @param path
*/
public static void deserializationFromFile(String path){
try(FileInputStream fileIn = new FileInputStream(path);
ObjectInputStream in = new ObjectInputStream(fileIn);){
Student s = (Student) in.readObject();
System.out.println(s.getAddress());
System.out.println(s.getAge());
System.out.println(s.getName());
}catch (Exception e){
e.printStackTrace();
}
}
}
上面使用了try-with-resources
可以自动关闭任何实现 java.lang.AutoCloseable
或者 java.io.Closeable
的对象。这样我们就不用在finally再关闭了。
主程序:
public class mainPractice {
public static void main(String[] args) throws IOException {
Student toBeSerializedObject = new Student("小明",16,"翻斗花园!");
StringBuffer path = new StringBuffer("E:");
path.append(File.separator).append("test").append(File.separator).append("student.ser");
SerializeOperation.serializeToFile(toBeSerializedObject, path.toString());
SerializeOperation.deserializationFromFile(path.toString());
}
}
5.2 JDK序列化的缺陷
我们在用过的RPC(远程方法调用)通信框架中,很少会发现使用JDK提供的序列化,主要是因为JDK默认的序列化存在着如下一些缺陷:
1. 无法跨语言
现在很多系统的复杂度很高,采用多种语言来编码,而Java序列化目前只支持Java语言实现的框架,其它语言大部分都没有使用Java的序列化框架,也没有实现Java序列化这套协议,因此,如果两个基于不同语言编写的应用程序之间通信,使用Java序列化,则无法实现两个应用服务之间传输对象的序列化和反序列化。 像JSON序列化的话就可以跨语言,因为JSON这种数据格式是通用的。
2. 易被攻击
Java官网安全编码指导方针里有说明,“对于不信任数据的反序列化,从本质上来说是危险的,应该避免“。可见Java序列化并不是安全的。
我们知道对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,这个方法其实是一个神奇的构造器,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这也就意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。
对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。
Set root = new HashSet();
Set s1 = root;
Set s2 = new HashSet();
for (int i = 0; i < 100; i++) {
Set t1 = new HashSet();
Set t2 = new HashSet();
t1.add("test"); //使t2不等于t1
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}
如何解决这个漏洞?
很多序列化协议都制定了一套数据结构来保存和获取对象。例如,JSON 序列化、ProtocolBuf 等,它们只支持一些基本类型和数组数据类型,这样可以避免反序列化创建一些不确定的实例。虽然它们的设计简单,但足以满足当前大部分系统的数据传输需求。我们也可以通过反序列化对象白名单来控制反序列化对象,可以重写 resolveClass 方法,并在该方法中校验对象名字。代码如下所示:
@Override
protected Class resolveClass(ObjectStreamClass desc) throws IOException,ClassNotFoundException {
if (!desc.getName().equals(Bicycle.class.getName())) {
throw new InvalidClassException(
"Unauthorized deserialization attempt", desc.getName());
}
return super.resolveClass(desc);
}
3. 序列化后的流太大
序列化后的二进制流大小能体现序列化的性能。序列化后的二进制数组越大,占用的存储空间就越多,存储硬件的成本就越高。如果我们是进行网络传输,则占用的带宽就更多,这时就会影响到系统的吞吐量。
Java 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,那么这种序列化机制实现的二进制编码完成的二进制数组大小,相比于 NIO 中的 ByteBuffer 实现的二进制编码完成的数组大小,要大上几倍。
4. 序列化性能太差
Java 序列化中的编码耗时要比 ByteBuffer 长很多。
5.3 Kryo
Kryo 是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。
另外,Kryo 已经是一种非常成熟的序列化实现了,已经在 Twitter、Groupon、Yahoo 以及多个著名开源项目(如 Hive、Storm)中广泛的使用。刚刚序列化和反序列化Student的案例在Kryo上使用如下:
public class KryoSerializerOperation {
public static void serializeToFile(Object toBeSerializedObject, String path){
Kryo kryo = new Kryo();
kryo.register(toBeSerializedObject.getClass());
try (Output output = new Output(new FileOutputStream(path));){
kryo.writeObject(output, toBeSerializedObject);
}catch (Exception e){
e.printStackTrace();
}
}
public static void deSerializeFromFile(Class toBeSerializedObject, String path){
Kryo kryo = new Kryo();
kryo.register(toBeSerializedObject);
try (Input input = new Input(new FileInputStream(path));){
Student s = (Student) kryo.readObject(input, toBeSerializedObject);
System.out.println(s);
}catch (Exception e){
e.printStackTrace();
}
}
}
主程序:
public class mainPractice {
public static void main(String[] args) {
KryoSerializerOperation.serializeToFile(ConstantUsedBySerialization.student
,ConstantUsedBySerialization.path);
KryoSerializerOperation.deSerializeFromFile(ConstantUsedBySerialization.student.getClass()
,ConstantUsedBySerialization.path);
}
}
其中的变量:
public class ConstantUsedBySerialization {
public static Student student = new Student("小明",16,"翻斗花园");
public static String path = "E:"+ File.separator+"test"+File.separator+"student.ser";
}
5.4 Protobuf
Protobuf 出自于 Google,性能还比较优秀,也支持多种语言,同时还是跨平台的。就是在使用中过于繁琐,因为你需要自己定义 IDL 文件和生成对应的序列化代码。这样虽然不然灵活,但是,另一方面导致 protobuf 没有序列化漏洞的风险。
Protobuf 包含序列化格式的定义、各种语言的库以及一个 IDL 编译器。正常情况下你需要定义 proto 文件,然后使用 IDL 编译器编译成你需要的语言
一个简单的 proto 文件如下:
// protobuf的版本
syntax = "proto3";
// SearchRequest会被编译成不同的编程语言的相应对象,比如Java中的class、Go中的struct
message Person {
//string类型字段
string name = 1;
// int 类型字段
int32 age = 2;
}
5.5 总结
Kryo 是专门针对 Java 语言序列化方式并且性能非常好,如果你的应用是专门针对 Java 语言的话可以考虑使用,并且 Dubbo 官网的一篇文章中提到说推荐使用 Kryo 作为生产环境的序列化方式。