一、Java 序列化与反序列化基础概念

1.1 什么是序列化与反序列化

在 Java 里,序列化就是把对象转化成字节序列的过程,就好像把一个大箱子里的东西整理好打包,方便运输和存储。而反序列化则是把字节序列再变回对象,就像把打包好的箱子打开,把里面的东西还原。

1.2 为什么需要序列化

在实际开发中,有很多场景需要用到序列化。比如,当你要把对象通过网络传输到其他地方,或者把对象保存到文件里,就需要先把对象序列化。举个例子,你在一个电商系统里,要把用户的订单信息保存到数据库里,就可以先把订单对象序列化,再存储到数据库中。

1.3 实现序列化的条件

要让一个类的对象可以被序列化,这个类必须实现 java.io.Serializable 接口。这个接口就像是一个标记,告诉 Java 虚拟机这个类的对象可以被序列化。下面是一个简单的示例:

// Java 技术栈
// 定义一个实现 Serializable 接口的类
import java.io.Serializable;

class User implements Serializable {
    private String name;
    private int age;

    // 构造函数
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter 和 Setter 方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

二、Java 序列化与反序列化的实现

2.1 序列化的实现

要实现序列化,需要使用 ObjectOutputStream 类。下面是一个完整的序列化示例:

// Java 技术栈
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class SerializationExample {
    public static void main(String[] args) {
        // 创建一个 User 对象
        User user = new User("Alice", 25);

        try (FileOutputStream fileOut = new FileOutputStream("user.ser");
             ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
            // 把 User 对象写入文件
            out.writeObject(user);
            System.out.println("对象已序列化并保存到 user.ser 文件中");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.2 反序列化的实现

反序列化使用 ObjectInputStream 类。下面是对应的反序列化示例:

// Java 技术栈
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class DeserializationExample {
    public static void main(String[] args) {
        try (FileInputStream fileIn = new FileInputStream("user.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn)) {
            // 从文件中读取对象
            User user = (User) in.readObject();
            System.out.println("反序列化成功");
            System.out.println("姓名: " + user.getName());
            System.out.println("年龄: " + user.getAge());
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

三、Java 序列化与反序列化的原理

3.1 序列化的原理

Java 序列化时,会把对象的状态信息(也就是对象的属性值)按照一定的格式转化成字节序列。这个过程会递归地处理对象的所有属性,如果属性也是对象,也会对其进行序列化。

3.2 反序列化的原理

反序列化时,会根据字节序列中的信息,重新创建对象,并把属性值赋值给对象。在反序列化过程中,Java 会调用对象的无参构造函数来创建对象,然后再把属性值设置进去。

四、Java 序列化与反序列化的应用场景

4.1 数据持久化

把对象保存到文件或者数据库中,方便后续使用。比如,游戏中的玩家数据,在玩家退出游戏时,可以把玩家的状态信息序列化保存到文件中,下次玩家登录时再反序列化恢复数据。

4.2 网络传输

在网络通信中,把对象序列化后通过网络传输到其他节点,然后在接收端反序列化。比如,在分布式系统中,不同节点之间需要交换对象信息,就可以使用序列化和反序列化。

4.3 缓存

把对象序列化后存储在缓存中,提高系统的性能。比如,在 Web 应用中,把经常使用的对象序列化后存储在 Redis 缓存中,下次需要使用时直接从缓存中反序列化获取。

五、Java 序列化与反序列化的优缺点

5.1 优点

  • 方便数据传输和存储:可以把对象转化成字节序列,方便在网络中传输和保存到文件或数据库中。
  • 跨平台:Java 序列化后的字节序列可以在不同的操作系统和 Java 虚拟机上进行反序列化。
  • 自动化:Java 提供了内置的序列化和反序列化机制,开发者只需要实现 Serializable 接口,就可以轻松实现对象的序列化和反序列化。

5.2 缺点

  • 性能问题:序列化和反序列化过程需要消耗一定的时间和资源,尤其是对于复杂的对象。
  • 版本兼容性问题:如果类的结构发生变化,可能会导致反序列化失败。比如,在类中添加或删除了属性,可能会影响反序列化的结果。
  • 安全问题:反序列化过程可能会存在安全漏洞,容易受到反序列化攻击。

六、反序列化攻击及防范

6.1 什么是反序列化攻击

反序列化攻击是指攻击者通过构造恶意的序列化数据,在反序列化过程中执行恶意代码。攻击者可以利用反序列化过程中的漏洞,获取系统的敏感信息、执行任意命令等。

6.2 反序列化攻击的原理

在反序列化过程中,Java 会根据字节序列中的信息创建对象,并调用对象的一些方法。攻击者可以构造恶意的字节序列,让 Java 在反序列化时调用一些危险的方法,从而实现攻击。

6.3 防范反序列化攻击的方法

  • 限制反序列化的类:只允许反序列化指定的类,避免反序列化未知的类。可以通过自定义 ObjectInputStream 类,重写 resolveClass 方法来实现。下面是一个示例:
// Java 技术栈
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;

public class SafeObjectInputStream extends ObjectInputStream {
    private static final String[] ALLOWED_CLASSES = {
            "com.example.User"
    };

    public SafeObjectInputStream(java.io.InputStream in) throws IOException {
        super(in);
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        for (String allowedClass : ALLOWED_CLASSES) {
            if (desc.getName().equals(allowedClass)) {
                return super.resolveClass(desc);
            }
        }
        throw new ClassNotFoundException("不允许反序列化该类: " + desc.getName());
    }
}
  • 验证输入数据:在反序列化之前,对输入的字节序列进行验证,确保数据的合法性。
  • 更新 Java 版本:及时更新 Java 版本,修复已知的反序列化漏洞。

七、注意事项

7.1 序列化版本号

在实现 Serializable 接口的类中,最好显式地定义 serialVersionUID。这个版本号可以保证在类的结构发生变化时,反序列化仍然可以正常进行。示例如下:

// Java 技术栈
import java.io.Serializable;

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    // 构造函数
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter 和 Setter 方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

7.2 静态和瞬态字段

静态字段和被 transient 关键字修饰的字段不会被序列化。静态字段属于类,而不是对象,所以不会被序列化。transient 关键字表示该字段不需要被序列化,在反序列化时,这些字段会被初始化为默认值。

八、文章总结

Java 序列化与反序列化是 Java 中非常重要的特性,它为数据的存储和传输提供了便利。通过实现 Serializable 接口,我们可以轻松地把对象序列化和反序列化。但是,我们也需要注意反序列化攻击的问题,采取相应的防范措施,如限制反序列化的类、验证输入数据等。同时,在使用序列化时,要注意序列化版本号、静态和瞬态字段等问题,以确保序列化和反序列化的正确性和安全性。