JAVA的序列化和反序列化
概述
1.概念
Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
序列化是将数据分解为字节流,一边存储在文件中或者是在网络上面传播;反序列化则是打开字节流并重构对象。
2.为什么需要JAVA的序列化和反序列化
两个JAVA进程之间想要实现进程间对象的传输,就需要用到JAVA的序列化和反序列化。
简单说就是:
发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
3.JAVA的序列化和反序列的一些应用的场景:
将内存中的对象把保存在一个文件或者是一个数据库中;
想用套接字在网络上面传输数据;
想要通过RMI传输对象。
这些应用场景涉及到的将对象转化为二进制,序列化保证了能够成功的读取到保存的对象。
4.常见的序列化和反序列化的协议
XML SOAP
JSON
Protobuf
JAVA中序列化的实现
实现了Serializable或者是Externalizable接口的类的对象才能够被序列化为字节序列,否则抛出异常。
Serializable接口概述
JAVA提供的一个序列化的接口(空接口)
public interface Serializable{
}
这个空接口的作用:
表示实现了这个接口的类可以被ObjectOutputSream序列化和被ObjectInputSream反序列化。
Serializable接口的基本使用
**ObjectOutputStream** 和 **ObjectInputStream** 是 Java 中用于对象序列化(Serialization)和反序列化(Deserialization)的两个核心类,属于 java.io 包。它们的作用是将对象转换为字节流(序列化)或从字节流重建对象(反序列化)。
serializable接口的利用的就是通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream(文件输出流) 将数据写入到文件中或者包装 ByteArrayOutStream(字节数组输入流) 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
重点就是ObjectOutputStream(对象输出流)和ObjectInputStream(对象输入流)这两个类的使用。
Serializable 接口的特点
1.序列化的类的属性也要实现Serializable类否则也会报错。
原因:
JAVA的序列化是递归的,序列化一个对象的时候会尝试序列化所有的成员变量。
2.父类未实现Serializable接口的序列化问题
序列化过程中父类没有实现Serializable接口的话,需要提供无参构造函数来重新创建对象
如下面这个测试实例:
首先是父类Animal类:
//没有实现Serializable接口
public class Animal {
private String color; // 颜色属性不会被序列化
// 必须有无参构造器,供子类反序列化时调用
public Animal() {
System.out.println("[Animal] 调用无参构造器");
}
public Animal(String color) {
this.color = color;
System.out.println("[Animal] 调用有参构造器,color=" + color);
}
@Override
public String toString() {
return "Animal{color='" + color + "'}";
}
// 添加getter/setter便于调试
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
}
子类BlackCat类
import java.io.Serializable;
/**
* 子类,实现了Serializable接口
*/
public class BlackCat extends Animal implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号
private String name; // 这个属性会被序列化
public BlackCat() {
super();
System.out.println("[BlackCat] 调用无参构造器");
}
public BlackCat(String color, String name) {
super(color);
this.name = name;
System.out.println("[BlackCat] 调用有参构造器,name=" + name);
}
@Override
public String toString() {
return "BlackCat{" +
"name='" + name + "', " +
super.toString() + // 包含父类的toString
"}";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
SuperMain测试类
import java.io.*;
public class SuperMain {
// 序列化文件路径(保存在项目根目录)
private static final String FILE_PATH = "blackcat.ser";
public static void main(String[] args) {
try {
System.out.println("============ 序列化开始 ============");
serializeAnimal();
System.out.println("\n============ 反序列化开始 ============");
deserializeAnimal();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 序列化BlackCat对象
*/
private static void serializeAnimal() throws IOException {
// 1. 创建对象
BlackCat cat = new BlackCat("黑色", "小黑");
System.out.println("序列化前对象: " + cat);
// 2. 创建输出流
FileOutputStream fos = new FileOutputStream(FILE_PATH);
ObjectOutputStream oos = new ObjectOutputStream(fos);
// 3. 写入对象
oos.writeObject(cat);
// 4. 关闭资源
oos.close();
fos.close();
System.out.println("序列化完成,文件已保存到: " + new File(FILE_PATH).getAbsolutePath());
}
/**
* 反序列化BlackCat对象
*/
private static void deserializeAnimal() throws IOException, ClassNotFoundException {
// 1. 创建输入流
FileInputStream fis = new FileInputStream(FILE_PATH);
ObjectInputStream ois = new ObjectInputStream(fis);
// 2. 读取对象
BlackCat cat = (BlackCat) ois.readObject();
// 3. 关闭资源
ois.close();
fis.close();
System.out.println("反序列化后对象: " + cat);
System.out.println("name属性: " + cat.getName()); // 正常输出
System.out.println("color属性: " + cat.getColor()); // 为null,因为父类未序列化
}
}
下面是这个项目测试的结果

首先可以看到Animal类中有一个color属性,Blackcat类中有一个name属性,这两个属性在类序列化的时候也会被序列化。
对比序列化前的对象和序列化之后的对象可以发现,Animal类中的color属性在序列化和反序列化的过程中null,这就是因为Animal最为父类但是没有实现Serialiazable接口,所以在序列化的过程中就需要无参构造器对Animal类的属性进行初始化,就会得到nall了。
3.实现 Serializable 接口的子类也是可以被序列化的
4. 静态成员变量是不能被序列化
JAVA的序列化是针对对象的属性的,而静态成员变量是属于类的。
5.transient 标识的对象成员变量不参与序列化
示例,还是上面的测试内容
在blackcat的name成员变量前面添加transient,序列化和反序列化的效果如下:


可以看到name序列化和反序列化之后的属性也变成了null。
6.Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片(涉及到反射后面再进行解释)
自定义序列化
在进行数组的序列化的时候,假设初始化的数组的长度是100,但是数组中实际上只是储存了30个数组的话,实际上进行序列化的时候仍然会将100个数组全部序列化,这时候就需要自定义序列化。
主要利用的是下面的两个方法:
writeObject(控制对象如何被写入字节流)
readObject(控制对象从字节流中恢复)
这两个方法常用形式:
private void writeObject(ObjectOutputStream out) throws IOException{
}
//ObjectOutputStream out :提供写入二进制数据的方法
//throws IOException:可能因 I/O 操作失败抛出异常。
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException{
}
//ObjectInputStream in:从字节流中反序列化(重建)Java 对象
//ClassNotFoundException:反序列化时如果类不存在会抛出。
序列化中readObject方法的安全问题
为什么产生安全性问题
只要服务器端反序列化数据,客户端传递的类的readObject方法中的代码就会自动执行,给予了攻击者在服务器上面运行代码的能力。
可能的形式:
1.入口类的readObject方法直接调用了危险函数
2.入口类的参数中包含了可控类,该类有危险方法,在readObject的时候被调用。
3.入口类的参数中包含了可控类,这个类又调用了其他有危险方法的类,readObject时候调用。
4.构造函数/静态代码块等类加载时候隐式执行。
要实现一个反序列化的攻击的步骤:
1.首先要找到一个入口类resource(参数的类型宽泛,调用常见的函数,重写readObject方法,最好为JDK自带)
2.找调用链(相同名称,相同类型)
3.执行类sink(rce ssrf 写文件)
序列化ID
在需要进行序列化的类中加上的一个serialVersionUID的字段这个就是序列化UID.(条件之一就是这个类需要实现了**Serializable 或 Externalizable 接口**)

序列化ID决定着能否成功的反序列化。原因:
java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。