ycycyc

JAVA序列化和反序列化

2025-07-23

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,因为父类未序列化
    }
}

下面是这个项目测试的结果

image-20250716153845979

首先可以看到Animal类中有一个color属性,Blackcat类中有一个name属性,这两个属性在类序列化的时候也会被序列化。

对比序列化前的对象和序列化之后的对象可以发现,Animal类中的color属性在序列化和反序列化的过程中null,这就是因为Animal最为父类但是没有实现Serialiazable接口,所以在序列化的过程中就需要无参构造器对Animal类的属性进行初始化,就会得到nall了。

3.实现 Serializable 接口的子类也是可以被序列化的

4. 静态成员变量是不能被序列化

JAVA的序列化是针对对象的属性的,而静态成员变量是属于类的。

5.transient 标识的对象成员变量不参与序列化

示例,还是上面的测试内容

在blackcat的name成员变量前面添加transient,序列化和反序列化的效果如下:

image-20250716201525018

image-20250716201539947

可以看到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.(条件之一就是这个类需要实现了**SerializableExternalizable 接口**)

image-20250716210836666

序列化ID决定着能否成功的反序列化。原因:

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

← Back to Home