你好,我是 shengjk1,多年大厂经验,努力构建 通俗易懂的、好玩的编程语言教程。 欢迎关注!你会有如下收益:
- 了解大厂经验
- 拥有和大厂相匹配的技术等
希望看什么,评论或者私信告诉我!
@TOC
一、背景
二、JDK 原生的序列化方式
Externalizable 和 Serializable 都是 Java 中用于对象序列化的机制,但它们在控制粒度、性能、使用复杂度等方面有显著区别。下面是两者的详细对比:
一、基本定义
| 特性 | Serializable |
Externalizable |
|---|---|---|
| 所在包 | java.io.Serializable |
java.io.Externalizable |
| 类型 | 标记接口(Marker Interface) (无方法) | 功能性接口 (需实现 writeExternal() 和 readExternal()) |
| 继承关系 | Externalizable extends Serializable |
是 Serializable 的子接口 |
二、核心区别对比表
| 对比维度 | Serializable |
Externalizable |
|---|---|---|
| 序列化控制 | 自动序列化所有非 transient/static 字段 |
完全手动控制:开发者决定哪些字段写入/读取 |
| 构造函数调用 | 反序列化时 不调用任何构造函数(直接分配内存) | 反序列化时 必须调用无参构造函数 ,再调用 readExternal() |
| 性能 | 较慢(反射 + 元数据开销大) | 更快(无反射,直接 I/O 操作) |
| 序列化体积 | 较大(包含类元信息、字段名等) | 更小(只存你需要的数据) |
| 灵活性 | 低(自动处理,难定制) | 高(可加密、压缩、版本兼容等) |
| 易用性 | 极简(只需实现接口) | 复杂(需手写逻辑,易出错) |
| 默认行为 | 支持 defaultWriteObject() / defaultReadObject() |
无默认行为,一切靠自己 |
| 父类字段处理 | 自动递归处理(只要父类也可序列化) | 不会自动处理父类字段,需手动写入/读取 |
| 安全性 | 较低(反序列化漏洞高发) | 相对可控(可校验输入、过滤恶意数据) |
| 适用场景 | 快速原型、简单本地持久化 | 高性能系统、网络传输、自定义格式 |
三、代码行为对比
1. Serializable 示例(自动)
java
class User implements Serializable {
private String name;
private int id;
// 自动序列化 name 和 id
}
2. Externalizable 示例(手动)
java
class User implements Externalizable {
private String name;
private int id;
public User() {} // 必须!
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(name); // 手动写
out.writeInt(id);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = in.readUTF(); // 手动读(顺序必须一致!)
id = in.readInt();
}
}
⚠️ 如果
User有父类字段,Externalizable不会自动保存/恢复它们,而Serializable会(前提是父类也实现了Serializable或有无参构造器等)。
四、反序列化过程差异
| 步骤 | Serializable |
Externalizable |
|---|---|---|
| 1. 创建对象 | JVM 直接分配内存,跳过构造函数 | 先调用 无参构造函数 ,再调用 readExternal() |
| 2. 恢复状态 | 通过反射设置字段值 | 通过 readExternal() 手动赋值 |
| 3. 完成 | 对象 ready | 对象 ready |
五、何时选择哪个?
用 Serializable 当:
- 快速实现序列化;
- 对象结构简单;
- 不关心性能或体积;
- 用于本地缓存、临时存储等。
用 Externalizable 当:
- 需要极致性能(如游戏、高频交易);
- 要控制序列化格式(如兼容旧协议);
- 需要加密/压缩序列化数据;
- 对象包含敏感字段,不想自动暴露;
- 希望减少网络传输体积。
六、替代方案建议(现代开发)
虽然两者都是 JDK 内置方案,但在实际生产中,更推荐使用第三方序列化框架:
| 场景 | 推荐方案 |
|---|---|
| Web API / 配置 | JSON(Jackson / Gson) |
| 微服务 / RPC | Protobuf / Thrift |
| 高性能 Java 应用 | Kryo / FST |
| 大数据(Spark/Flink) | Kryo |
这些方案通常比 Externalizable 更高效、更安全、更易维护。
总结一句话:
Serializable是"全自动挡",Externalizable是"手动挡"------前者省事,后者精准可控。
三、JDK 原生的序列化漏洞RCE
Java
import java.io.*;
import java.util.Base64;
public class SerializeDemo {
// 1. 必须实现 Serializable 接口
static class User implements Serializable {
// 建议显式声明 serialVersionUID(否则每次代码改动都会变,导致反序列化失败)
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // transient 字段不会被序列化(敏感信息推荐)
private int age;
public User(String username, String password, int age) {
this.username = username;
this.password = password;
this.age = age;
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
System.out.println("writeObject 被调用了!!!");
}
// 关键是在这里,如果服务端接收一个序列化后的对象,反序列化的时候就有可能
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
System.out.println("readObject 我被调用了!!!");
in.defaultReadObject();
// Runtime.getRuntime().exec("calc"); // Windows
Runtime.getRuntime().exec("open -a Calculator.app"); // macOS
// Runtime.getRuntime().exec("gedit"); // Linux
}
@Override
public String toString() {
return "User{username='" + username + "', password='" + password + "', age=" + age + '}';
}
}
public static void main(String[] args) throws Exception {
User user = new User("admin", "123456", 18);
// ================ 序列化到字节数组(最常用)===============
byte[] bytes = serialize(user);
// 方便传输/存储,通常会转成 Base64 字符串
String base64 = Base64.getEncoder().encodeToString(bytes);
System.out.println("Base64 序列化结果:");
System.out.println(base64);
// ================ 反序列化 ================
User deserializedUser = (User) deserialize(bytes);
System.out.println("反序列化后对象:" + deserializedUser);
}
// 序列化方法(返回 byte[])
public static byte[] serialize(Object obj) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
System.out.println("================= 序列化开始 ================");
oos.writeObject(obj);
oos.flush();
return baos.toByteArray();
}
}
// 反序列化方法(这就是高危入口!)
public static Object deserialize(byte[] data) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais)) {
System.out.println("================= 反序列化开始 ================");
return ois.readObject(); // ← 这里就是所有 Java 反序列化漏洞的根源
}
}
// 方法1:JVM 启动参数全局过滤(推荐用于无法改代码的老项目)
// -Djdk.serialFilter=*!com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;*!java.lang.Runtime;*!java.lang.ProcessBuilder;maxdepth=20;maxrefs=10000
// 方法3:每次创建 ObjectInputStream 时单独设置(最灵活)
public static Object safeDeserialize(byte[] data) throws Exception {
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ObjectInputStream ois = new ObjectInputStream(bais) {
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
String name = desc.getName();
// 白名单(推荐!只允许你自己的类)
if (!name.startsWith("com.yourcompany.yourapp.") &&
!name.startsWith("[Lcom.yourcompany.yourapp.") && // 数组
!name.equals("java.lang.String") &&
!name.startsWith("java.util.") &&
!name.startsWith("java.lang.")) {
throw new InvalidClassException("Unauthorized class", name);
}
// 或者用黑名单(不推荐,容易被绕过)
if (name.contains("TemplatesImpl") ||
name.contains("AnnotationInvocationHandler") ||
name.contains("Runtime") ||
name.contains("ProcessBuilder")) {
throw new InvalidClassException("Blocked dangerous class", name);
}
return super.resolveClass(desc);
}
}) {
return ois.readObject();
}
}
}