ObjectInputStream / ObjectOutputStream 序列化源码精读
概述
Java 原生序列化把对象图写入字节流,再在另一端还原。核心是 ObjectStreamClass(类结构描述符)、ObjectStreamField(字段描述)与一套类型标记协议。原生序列化的反射开销大、安全风险高(反序列化攻击),理解源码后才能正确取舍 Externalizable、writeReplace 与 ObjectInputFilter。本文基于 OpenJDK 21 源码拆解。
一、ObjectOutputStream.writeObject(Object) 流程
java
// java.io.ObjectOutputStream
public final void writeObject(Object obj) throws IOException {
if (enableReplace) { ... }
writeObject0(obj, false); // 核心
}
private void writeObject0(Object obj, boolean unshared) throws IOException {
...
// ① 校验类可序列化
if (obj instanceof String) { writeString(obj, unshared); return; }
if (obj instanceof Enum) { writeEnum(obj, unshared); return; }
if (obj instanceof Class) { writeClass(obj, unshared); return; }
// ② 找类描述符(缓存)
ObjectStreamClass desc = ObjectStreamClass.lookup(obj.getClass());
...
// ③ 按类型写入
if (obj instanceof Externalizable) {
writeExternalData(obj); // Externalizable 手动序列化
} else {
writeSerialData(obj, desc); // Serializable 默认序列化
}
...
}writeObject 流程:
writeObject0 分派:String/Enum/Class/数组/对象各自处理
对象 → ObjectStreamClass.lookup 拿描述符
Externalizable → 调 writeExternal()(用户手动写)
Serializable → writeSerialData() 反射写字段
写过的对象记入 handle 表 → 循环引用/重复引用复用句柄二、ObjectStreamClass 描述符缓存
java
// java.io.ObjectStreamClass
private static final ConcurrentHashMap<Class<?>, ObjectStreamClass> cache
= new ConcurrentHashMap<>(); // 类 → 描述符 的全局缓存
public static ObjectStreamClass lookup(Class<?> cl) {
return lookup(cl, false);
}
static ObjectStreamClass lookup(Class<?> cl, boolean all) {
ObjectStreamClass desc = cache.get(cl); // ① 缓存命中
if (desc == null) {
desc = new ObjectStreamClass(cl); // ② 构建描述符
if (desc.isProxy || desc.isSerializable) {
cache.putIfAbsent(cl, desc); // ③ 放入缓存
return cache.get(cl);
}
}
return desc;
}ObjectStreamClass 描述符包含:serialVersionUID、字段表(ObjectStreamField[])、父类链等——一次构建、全局复用,避免每次序列化都反射解析类结构。
三、getSerialVersionUID() 的计算
java
public long getSerialVersionUID() {
// 懒计算
if (suid == null) {
suid = computeDefaultSUID(cl); // 未显式声明 → 计算
}
return suid.longValue();
}serialVersionUID 规则:
① 类显式声明 static final long serialVersionUID → 直接用
② 未声明 → 按类结构计算默认值:
类名、修饰符、接口、字段(名字+类型+修饰符)、方法签名
用 SHA-1 对类结构数据哈希 → 取前 8 字节作为 UID
注意:
类结构任何变化(加字段/改方法签名)→ 默认 UID 变化
→ 反序列化旧数据抛 InvalidClassException
→ 显式声明 UID 可控制兼容性(同一 UID 则字段匹配即可)四、writeSerialData 默认序列化
java
// ObjectOutputStream.writeSerialData
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException {
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slotDesc.hasWriteObjectMethod()) {
// 类自定义 writeObject() → 调用(写入超类字段由 writeObject 内 super 处理)
slotDesc.invokeWriteObject(obj, this);
} else {
// 默认:反射遍历字段逐个写入
defaultWriteFields(obj, slotDesc);
}
}
}
private void defaultWriteFields(Object obj, ObjectStreamClass desc) {
ObjectStreamField[] fields = desc.getFields();
for (ObjectStreamField f : fields) {
// 反射读取字段值 → 按字段类型写入流
Object val = f.getField().get(obj);
writeObject0(val, false);
}
}默认序列化字段规则:
遍历 ObjectStreamField[](非 transient、非 static 的字段)
按字段类型写入:primitive 直接写字节,对象字段递归 writeObject0
超类字段通过 ClassDataSlot 分层写入(先父类后子类)五、readObject() 反序列化还原
java
public final Object readObject() throws ... {
return readObject0(false); // 核心
}
private Object readObject0(boolean unshared) throws ... {
byte tc = bin.peekByte(); // ① 读类型标记
switch (tc) {
case TC_STRING: return readString(unshared);
case TC_ENUM: return readEnum(unshared);
case TC_OBJECT: return readOrdinaryObject(unshared); // 普通对象
case TC_ARRAY: return readArray(unshared);
...
}
}
private Object readOrdinaryObject(boolean unshared) throws ... {
// ① 读类描述符
ObjectStreamClass desc = readClassDesc(false);
...
// ② 分配实例(绕过构造器)
Object obj = desc.isInstantiable() ? desc.newInstance() : null;
...
// ③ 填充字段
readSerialData(obj, desc);
// ④ readResolve 替换
if (obj != null && desc.hasReadResolveMethod()) { ... }
return obj;
}反序列化还原流程:
readStreamHeader 校验流头(STREAM_MAGIC + STREAM_VERSION)
readClassDesc → 按描述符找本地类(resolveClass)
newInstance → 分配对象(绕过构造器,不执行构造逻辑)
readSerialData → 反射写入字段(还原状态)
readResolve → 有则替换对象(见第七节)java
// ObjectStreamClass.newInstance(绕过构造器)
Object newInstance() {
// 用反射的 Constructor.newInstance(不执行构造器体?实际走
// sun.reflect.ReflectionFactory 的 Unsafe 分配或特殊构造函数)
return cons.newInstance();
}反序列化不执行构造函数,字段直接赋值——这是反序列化与普通 new 的本质区别,也是许多漏洞的入口(构造器校验被跳过)。
六、writeReplace() / readResolve() 代理机制
java
// 序列化时:writeReplace 返回代理对象
// ObjectOutputStream.writeObject0 内部:
ObjectStreamClass desc = ObjectStreamClass.lookup(obj.getClass());
if (desc.hasWriteReplaceMethod()) {
Object rep = desc.invokeWriteReplace(obj); // 调 writeReplace()
obj = rep; // 用代理替换原对象继续写
}
// 反序列化时:readResolve 还原
// ObjectInputStream.readOrdinaryObject 内部:
if (obj != null && desc.hasReadResolveMethod()) {
Object rep = desc.invokeReadResolve(obj); // 调 readResolve()
obj = rep; // 替换为最终对象
}java
// 典型应用:单例/枚举保护的序列化
private Object readResolve() {
return getInstance(); // 反序列化时返回已有单例,不新建
}writeReplace / readResolve:
写入端:writeReplace() → 代理对象写入流
读取端:readResolve() → 还原为真实对象
用途:单例保护、枚举反序列化(枚举另有原生机制)、
DTO 隐藏内部结构、版本兼容适配七、ObjectInputFilter 安全过滤
java
// JDK 9+:设置全局序列化过滤器
ObjectInputFilter.Config.setSerialFilter(
ObjectInputFilter.Config.createFilter(
"maxdepth=10;java.base/*;!java.lang.Runtime"
)
);
// ObjectInputStream 内部
private ObjectInputFilter filter; // 流级过滤器
private void filterCheck(Class<?> clazz, int depth) {
// 对每个反序列化的类调用过滤器
if (filter != null) {
ObjectInputFilter.Status status = filter.checkInput(new FilterValues(clazz, ...));
if (status == Status.REJECTED) throw new InvalidClassException(...);
}
}序列化攻击防护:
反序列化 → 任意类实例化 → 可能触发危险类(Runtime/脚本引擎)的
readObject 逻辑 → 远程代码执行(RCE)
过滤器:checkInput 按规则拒绝危险类/限制深度/大小
规则语法:模式 + 类名通配(java.base/* 允许、!java.lang.Runtime 拒绝)八、Serializable vs Externalizable 与 transient
8.1 两种序列化方式
| 维度 | Serializable | Externalizable |
|---|---|---|
| 控制方式 | 反射自动(或 writeObject 定制) | writeExternal/readExternal 手动 |
| 性能 | 反射开销大 | 无反射,可精确控制 |
| 字段 | 默认全量(transient 除外) | 完全自己写 |
| 兼容性 | 有版本机制(UID) | 无自动兼容 |
| 复杂度 | 简单 | 需自己处理字段/超类 |
java
// Externalizable 手动序列化
class Point implements Externalizable {
int x, y;
public void writeExternal(ObjectOutput out) {
out.writeInt(x); out.writeInt(y); // 精确控制,无反射
}
public void readExternal(ObjectInput in) {
x = in.readInt(); y = in.readInt();
}
}8.2 transient 字段跳过
java
// ObjectStreamClass 构造时过滤字段
private void buildFieldList() {
// 遍历类所有字段:
// static 字段 → 排除
// transient 字段 → 排除(不入 ObjectStreamField[])
// 其余 → 加入字段表
}transient 语义:
声明 transient 的字段不参与默认序列化
适合:密码、缓存、临时计算状态、不可序列化的资源句柄
注意:自定义 writeObject 时可以手动写 transient 字段(自行决定)九、实现要点
序列化核心:
writeObject0 分派:String/Enum/Class/对象/数组各自协议
ObjectStreamClass:ConcurrentHashMap 缓存类描述符
serialVersionUID:显式声明或按类结构 SHA-1 计算
默认序列化:ObjectStreamField 反射字段分层写入
反序列化:newInstance 绕过构造器 + 反射填字段
writeReplace/readResolve:写端代理 + 读端还原
ObjectInputFilter:JDK 9+ 过滤危险类防反序列化攻击
Externalizable:手动序列化避免反射
transient:默认序列化排除的字段
常见陷阱:
类结构变化 → 默认 UID 变化 → 反序列化失败(显式声明 UID)
反序列化不执行构造器 → 校验逻辑被跳过(安全风险)
未设过滤器 → 反序列化攻击风险(生产必须配 ObjectInputFilter)
继承链上无参构造要求 → 反序列化需要父类可实例化
不建议用原生序列化做跨语言/长存 → 用 JSON/Protobuf