开始前,会这些就够了
- 知道 Serializable、对象字段,以及
ObjectOutputStream写入和ObjectInputStream读回的基本用法。 - 先读:反序列化会重新调用构造器吗?,区分对象恢复和业务字段校验。
- 使用完整 JDK 17 或更新版本;本实验只读取自己刚生成的内存字节,不需要文件、网络或第三方库。
你写了一个规则:Allowed.class 返回允许,其他类型返回 UNDECIDED。名字看起来像“只允许 Allowed”,结果 Other 也被读出来了。问题在于 UNDECIDED 表示这次检查没有作出判断,并不是拒绝。
下面所有样本都由当前程序先写成内存字节,再从新建输入流读回。我们同时检查读回的对象和过滤回调记录,以免“得到结果”被误解为“某个条件一定被检查过”。
把例子带到自己的电脑上
下载完整练习包
需要 JDK 17 或更新版本,使用 Java 标准库;制作时实测 Windows / JDK 17.0.20.1 / 21.0.12。附加矩阵程序也只用Java标准库,已独立实测JDK17/21。下方命令运行基础 Java 标准库示例;附加对照的命令见对应 README。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 13 个文件包里有什么
- ObjectFilterDemo.java:完整可运行示例
- README.md:运行说明与独立练习
- observed.stdout.txt:本次实际输出
- verification.json:源码和产物验证记录
- SHA256SUMS.txt:内容摘要
- filter-evidence/FilterMatrixDemo.java:24格附加实验源码
- filter-evidence/README.md:附加运行方法与观察边界
- filter-evidence/matrix.json:双JDK一致的实际矩阵
- filter-evidence/matrix-jdk17.json / matrix-jdk21.json:分版本实际输出
- filter-evidence/validation.json / SHA256SUMS.txt:制作验证与文件摘要
- supplement-verification.json:附加实现的独立复跑记录
在这个文件夹打开 PowerShell,逐行运行
java -version
javac -version
javac -encoding UTF-8 --release 17 ObjectFilterDemo.java
java -cp . ObjectFilterDemo解压到独立练习文件夹,在含 ObjectFilterDemo.java 的目录打开终端运行。基础 ObjectFilterDemo 在 JDK17/21 各21项检查;附加 FilterMatrixDemo 在两版各105项检查,包括6个固定自产样本×4个策略的24格实际结果、回调类型/长度/状态、读回值和直接String零回调。
两份程序独立计数;没有外部输入或浏览器Java模拟。 附加实验进入 filter-evidence 目录,先执行 javac -encoding UTF-8 --release 17 FilterMatrixDemo.java,再执行 java -cp . FilterMatrixDemo。矩阵JSON是固定实际记录。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
六种样本遇到四种策略,Java 实际记录了什么?
这里展示固定 Java 程序已经运行得到的 24 格观察记录。选择样本与策略,只会切换对应记录;浏览器没有执行或模拟 Java 反序列化。
Allowed.class 得到 ALLOWED,其他非空类型得到 UNDECIDED;未决不是拒绝。
预设样本:Other(7)。实际结果:读回成功。
读回类型:Other;读回值:7。
对象流调用外层过滤器 1 次。下面按实际发生顺序列出每次回调与最终策略状态。
| 次序 | class(简名) | arrayLength | 最终状态 |
|---|---|---|---|
| 1 | Other | -1 | UNDECIDED |
arrayLength 为 −1 表示这次回调没有可用数组长度。数组可能先出现 int[] / −1 的类型事件,再出现长度为 3 或 5 的数组事件。被拒绝时没有完整读回对象,不把已知输入类型填成读回结果。
查看这格原始 JSON 记录
{
"sampleId": "other-7",
"sampleLabel": "Other(7)",
"policyId": "loose",
"policyLabel": "Allowed -> ALLOWED; others -> UNDECIDED",
"outcome": "read",
"observedType": "Other",
"observedValue": 7,
"exceptionType": null,
"callbacks": [
{
"class": "Other",
"arrayLength": -1,
"status": "UNDECIDED"
}
]
}展开完整 24 格实际矩阵
| 固定样本 | 允许 Allowed,其余未决(loose) | 拒绝未决类(strict) | 只限制数组最多 4 个元素 | 组合 strict 与数组上限 |
|---|---|---|---|---|
| Allowed(9) | 读回 · 1 次回调 | 读回 · 1 次回调 | 读回 · 1 次回调 | 读回 · 1 次回调 |
| Allowed(-9) | 读回 · 1 次回调 | 读回 · 1 次回调 | 读回 · 1 次回调 | 读回 · 1 次回调 |
| Other(7) | 读回 · 1 次回调 | 拒绝 · 1 次回调 | 读回 · 1 次回调 | 拒绝 · 1 次回调 |
| int[3] = [1,2,3] | 读回 · 2 次回调 | 读回 · 2 次回调 | 读回 · 2 次回调 | 读回 · 2 次回调 |
| int[5] = [1,2,3,4,5] | 读回 · 2 次回调 | 读回 · 2 次回调 | 拒绝 · 2 次回调 | 拒绝 · 2 次回调 |
| String own-text | 读回 · 0 次回调 | 读回 · 0 次回调 | 读回 · 0 次回调 | 读回 · 0 次回调 |
实测日期:2026-10-18。Microsoft OpenJDK 17.0.20.1 与 Microsoft OpenJDK 21.0.12 使用同一源码,以 --release 17 编译运行,并在独立目录复跑;两版 JSON 一致。附加程序每版 105 项检查,其中 24 项核对矩阵结果。原文基础程序的 21 项检查单独计数。
全部字节由程序自己构造的六个样本产生。这组记录不覆盖任意对象图、外部字节、全局过滤器工厂或完整资源策略;共享引用与 class 为 null 的情况仍见正文的基础实验。
先用两个只含整数的类建立对照
Allowed 和 Other 是两个独立 Serializable 类,都只含一个 int。每个样本建立独立内存对象流;Trace 包装实际过滤器,保存此次回调的类型、数组长度、深度、引用数、已消耗字节数和返回状态。
decodeOwn 在 readObject 之前设置过滤器,并核对实际流过滤器就是所请求的对象。自定义全局过滤器工厂可能替换或组合规则;若发生这种情况,本实验明确停止,而不是继续假定运行环境仍与示例一致。它不修改全局过滤配置。
Trace 是观察工具,不读取业务字段,也不会把类型对象传成业务对象。后面通过不同 Trace 实例隔离每个样本,避免上一条流留下的计数混进下一条结论。
UNDECIDED 会继续,严格包装才拒绝这个普通类
loose 使用 allowFilter:Allowed 返回 ALLOWED,其他非空类型返回 UNDECIDED。读 Other(7) 后,程序既检查 value 仍为七,也检查回调确实见到 Other.class 和 UNDECIDED,证明它不是跳过了类型回调。
strict 用 rejectUndecidedClass 包装 loose。对完全相同的 Other 字节,回调结果变为 REJECTED,readObject 抛 InvalidClassException;Allowed(9) 仍能读回。我们检查异常类型和拒绝记录,避免把其他读流失败也当作过滤规则正确。
另外,Allowed(-9) 同样通过这条类型规则。这个类并未规定金额不能为负,过滤器也没有读取 value。输出 business_value 单独保留负九,提醒你类型获得允许并不能证明字段满足某项业务限制。
serialClass 为 null,可能仍有指标需要检查
程序把同一个 Allowed 对象放进数组两次。读回后 pair[0] == pair[1],说明共享引用被保留;回调记录中也出现 serialClass 为 null、arrayLength 为 -1 的事件。这里没有一个新的类需要判断,但仍能检查对象图的深度、引用和读入量。
示例过滤器先处理 type == null,再判断 Object[].class 和 Allowed.class。若直接调用 type.getName,就可能在这类合法回调中抛异常。返回值必须是 Status;serialClass 为 null 和“过滤器返回 null”是两件事,后者会使当前读取失败。
空类事件的指标检查只确认深度、引用数和字节数可用,不硬编码字节偏移。FilterInfo 文档说明 streamBytes 计数与实现有关,不能把某次回调的数值直接解释为当前对象的精确序列化大小。
数组长度零、未知和超过上限不是同一情况
maxFour 只检查 arrayLength > 4。程序分别写出长度零、三、四、五的 int 数组:前三种可以读回,五被拒绝。除了读回长度或异常,Trace 还核对真正带这些非负长度的数组回调;零长度与非数组事件的 -1 分开处理。
数组检查面对的是 int[].class 和实际长度。它不是对每个 int 元素单独调用类型过滤,也不会因为长度允许就验证数组内容。长度五的样本只是小数组,不需要制造大分配就能检验 > 4 的边界。
再将长度三的 int 数组交给前述 strict,仍得到 UNDECIDED 并成功读回。rejectUndecidedClass 对基本类型数组有专门规则,所以不能把它概括为“任何未决事件全部拒绝”。真正的数组长度上限应明确写出来;本文没有把这几个分离实验拼成一套完整生产规则。
直接写入的 String,拒绝回调一次也没有运行
最后只写入字符串 own-text,并安装一个只要被调用就返回 REJECTED 的 Trace。结果字符串仍然读出,Trace.events 却为空。程序既核对内容,也核对零次回调,这才支持“这个路径没有调用过滤器”的结论。
ObjectInputStream 对流中直接编码的 String 有明确特殊处理。这里没有说对象图里出现字符串就绕过所有过滤:外层普通对象、数组和重复引用仍各有自己的检查规则。实验只覆盖独立直接字符串样本。
因此,如果业务要求文本长度、格式或允许字符,应在合适的内容处理边界实现并测试这些规则,不能以为一个拒绝类型的回调已经审查过文本。类型、资源量和业务值是三个需要分别举证的问题。
用观察记录说明检查了什么,也说明没检查什么
21 项检查将普通类状态、读回字段、共享引用和空类事件、数组四个边界、基本类型数组特殊规则、直接字符串零回调分开验证。Windows 上两套 JDK 的输出一致,但程序没有依赖每条流的总回调次数或某个固定字节偏移。
所有输入都是当前程序自产的内存字节,示例没有接收外部文件,也不修改序列化二进制内容。它不实现自定义全局过滤器工厂,没有测试复杂类继承、代理、readResolve 或多层对象图资源限制。
已有构造路径文章讨论恢复后在哪里验证字段;本文补的是过滤器到底作出了哪种判断。理解这两个入口有助于设计检查,但这组小实验不构成一个外部反序列化接口的完整验收。
运行单文件,逐项对照真实输出
保存为 ObjectFilterDemo.java,在源码目录逐行运行下面命令。需要包含 javac 的完整 JDK 17 或更新版本;无需 Maven、网络或第三方依赖。
2026 年 10 月 18 日,已在 Windows 上分别使用 Microsoft OpenJDK 17.0.20.1 与 21.0.12,以 --release 17 在新建临时目录编译运行。下面直接保存两次一致的真实输出;断言失败不会继续打印 PASS。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class ObjectFilterDemo {
private static int checks;
static final class Allowed implements Serializable {
@Serial private static final long serialVersionUID = 1L;
final int value;
Allowed(int value) { this.value = value; }
}
static final class Other implements Serializable {
@Serial private static final long serialVersionUID = 1L;
final int value;
Other(int value) { this.value = value; }
}
record Event(Class<?> type, long length, long depth, long refs, long bytes,
ObjectInputFilter.Status status) {}
static final class Trace implements ObjectInputFilter {
final ObjectInputFilter delegate;
final List<Event> events = new ArrayList<>();
Trace(ObjectInputFilter delegate) { this.delegate = delegate; }
@Override public Status checkInput(FilterInfo info) {
Status status = delegate.checkInput(info);
events.add(new Event(info.serialClass(), info.arrayLength(), info.depth(),
info.references(), info.streamBytes(), status));
return status;
}
boolean saw(Class<?> type, long length, Status status) {
return events.stream().anyMatch(e -> e.type() == type && e.length() == length && e.status() == status);
}
}
private static void check(boolean ok, String message) {
if (!ok) throw new AssertionError(message);
checks++;
}
private static byte[] encodeOwn(Serializable value) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { output.writeObject(value); }
return bytes.toByteArray();
}
private static Object decodeOwn(byte[] bytes, ObjectInputFilter filter) throws IOException, ClassNotFoundException {
try (ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
input.setObjectInputFilter(filter);
// Do not silently run this isolated experiment under a custom filter factory.
if (input.getObjectInputFilter() != filter)
throw new IllegalStateException("experiment requires the requested stream filter");
return input.readObject();
}
}
private static boolean rejected(byte[] bytes, ObjectInputFilter filter) throws Exception {
try { decodeOwn(bytes, filter); return false; }
catch (InvalidClassException expected) { return true; }
}
public static void main(String[] args) throws Exception {
ObjectInputFilter loose = ObjectInputFilter.allowFilter(
type -> type == Allowed.class, ObjectInputFilter.Status.UNDECIDED);
byte[] otherBytes = encodeOwn(new Other(7));
Trace looseTrace = new Trace(loose);
Other other = (Other) decodeOwn(otherBytes, looseTrace);
check(other.value == 7, "undecided Other still restored");
check(looseTrace.saw(Other.class, -1, ObjectInputFilter.Status.UNDECIDED), "Other class was actually checked");
ObjectInputFilter strict = ObjectInputFilter.rejectUndecidedClass(loose);
Trace strictTrace = new Trace(strict);
check(rejected(otherBytes, strictTrace), "undecided ordinary class now rejected");
check(strictTrace.saw(Other.class, -1, ObjectInputFilter.Status.REJECTED), "record rejected status");
Allowed allowed = (Allowed) decodeOwn(encodeOwn(new Allowed(9)), strict);
check(allowed.value == 9, "explicitly allowed class succeeds");
System.out.println("classes: Other+UNDECIDED=read; Other+strict=InvalidClassException; Allowed+strict=read");
Allowed negative = (Allowed) decodeOwn(encodeOwn(new Allowed(-9)), strict);
check(negative.value == -9, "type-only rule does not validate the integer field");
System.out.println("business_value: Allowed.value=-9; type_filter=read");
// An existing reference triggers a metrics event with no new class to check.
Allowed shared = new Allowed(11);
Trace references = new Trace(info -> {
Class<?> type = info.serialClass();
if (type == null) return ObjectInputFilter.Status.UNDECIDED;
return type == Object[].class || type == Allowed.class
? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED;
});
Object[] pair = (Object[]) decodeOwn(encodeOwn(new Object[] {shared, shared}), references);
check(pair.length == 2 && pair[0] == pair[1], "shared reference restored as shared");
check(references.saw(null, -1, ObjectInputFilter.Status.UNDECIDED), "null class metrics event exists");
check(references.events.stream().filter(e -> e.type() == null)
.allMatch(e -> e.depth() >= 1 && e.refs() >= 1 && e.bytes() >= 0), "metrics available without a class");
System.out.println("references: shared_identity=true; serialClass_null=true; arrayLength=-1; metrics_present=true");
ObjectInputFilter maxFour = info -> info.arrayLength() > 4
? ObjectInputFilter.Status.REJECTED : ObjectInputFilter.Status.UNDECIDED;
Trace small = new Trace(maxFour);
int[] three = (int[]) decodeOwn(encodeOwn(new int[] {1, 2, 3}), small);
check(three.length == 3 && three[2] == 3, "small primitive array restored");
check(small.saw(int[].class, 3, ObjectInputFilter.Status.UNDECIDED), "actual length-three event");
Trace emptyTrace = new Trace(maxFour);
check(((int[]) decodeOwn(encodeOwn(new int[0]), emptyTrace)).length == 0, "empty array accepted");
check(emptyTrace.saw(int[].class, 0, ObjectInputFilter.Status.UNDECIDED), "zero length is not minus one");
Trace boundary = new Trace(maxFour);
check(((int[]) decodeOwn(encodeOwn(new int[4]), boundary)).length == 4, "exact maximum accepted");
check(boundary.saw(int[].class, 4, ObjectInputFilter.Status.UNDECIDED), "actual length-four event");
Trace large = new Trace(maxFour);
check(rejected(encodeOwn(new int[5]), large), "length-five array rejected");
check(large.saw(int[].class, 5, ObjectInputFilter.Status.REJECTED), "actual length-five rejection event");
Trace primitiveStrict = new Trace(strict);
check(((int[]) decodeOwn(encodeOwn(new int[3]), primitiveStrict)).length == 3,
"rejectUndecidedClass still permits undecided primitive arrays");
check(primitiveStrict.saw(int[].class, 3, ObjectInputFilter.Status.UNDECIDED), "primitive array special case recorded");
System.out.println("arrays: length0_3_4=read; length5=InvalidClassException; primitive_array_under_strict=UNDECIDED_read");
Trace stringTrace = new Trace(info -> ObjectInputFilter.Status.REJECTED);
Object text = decodeOwn(encodeOwn("own-text"), stringTrace);
check("own-text".equals(text), "direct String restored");
check(stringTrace.events.isEmpty(), "direct String invokes no filter callback");
System.out.println("direct_string: value=own-text; reject_all_callback_calls=0; read=true");
System.out.println("PASS checks=" + checks + "; input=program_generated_byte_arrays_only");
}
}
怎样运行:在源码所在目录,先单独执行 javac -encoding UTF-8 --release 17 ObjectFilterDemo.java;编译成功后,再执行 java -cp . ObjectFilterDemo。两条命令分别回车。2026-10-18 已在 Windows、JDK 17.0.20.1 / 21.0.12 编译运行此份源码并通过检查。Microsoft OpenJDK17/21各21项检查;自产内存流,普通类UNDECIDED/REJECTED与业务字段、serialClass=null指标事件、共享身份、数组0/3/4/5边界、基本类型数组特例、直接String零回调。未测外部输入、完整资源策略或自定义全局过滤工厂。
运行后对照结果
classes: Other+UNDECIDED=read; Other+strict=InvalidClassException; Allowed+strict=read
business_value: Allowed.value=-9; type_filter=read
references: shared_identity=true; serialClass_null=true; arrayLength=-1; metrics_present=true
arrays: length0_3_4=read; length5=InvalidClassException; primitive_array_under_strict=UNDECIDED_read
direct_string: value=own-text; reject_all_callback_calls=0; read=true
PASS checks=21; input=program_generated_byte_arrays_only附加实验:同样六个样本,交给四种策略逐格观察
上面的 ObjectFilterDemo 仍是基础实验,21项检查不变。下面另附 FilterMatrixDemo:六个固定样本逐一搭配四种策略,保存24格真实读取结果、返回值和回调顺序。浏览器交互只展示这些实际记录,不在JavaScript里模拟Java反序列化。
loose只允许Allowed,其余返回UNDECIDED;strict用rejectUndecidedClass包装它;maxArray4只拒绝超过四个元素的数组;merge将strict与maxArray4组合。每格建立新的对象流,核对请求的过滤器确实安装,并且只读本程序自产的内存字节。
两版JDK的24格输出一致。每版附加程序通过105项检查,其中24项是逐格结果;其他检查覆盖实际类型和值、拒绝回调、字符串零回调、数组长度和完整性。这个数字独立于原程序的21项,不把两种验证混成一个计数。
被拒绝时没有完整读回对象,observedType与observedValue留空。数组可能先出现class为int[]但arrayLength为-1的类型事件,再出现实际长度事件;回调表保留二者。这里未新增共享引用样本,serialClass为null的场景仍见基础实验。
Allowed(9)与Allowed(-9):四种策略均读回,说明类型允许没有代替业务值检查。Other(7):loose和maxArray4读回;strict与merge拒绝。int[3]:四种策略均读回;int[5]:loose和strict读回,maxArray4和merge拒绝。- 直接
Stringown-text:四种策略均读回,过滤回调都是零次。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** Fixed local observations, not an input service or a browser simulation. */
public class FilterMatrixDemo {
private static int checks;
private static int outcomeChecks;
static final class Allowed implements Serializable {
@Serial private static final long serialVersionUID = 1L;
final int value;
Allowed(int value) { this.value = value; }
}
static final class Other implements Serializable {
@Serial private static final long serialVersionUID = 1L;
final int value;
Other(int value) { this.value = value; }
}
record Sample(String id, String label, Serializable value) {}
record Policy(String id, String label, ObjectInputFilter filter) {}
record Callback(String className, long arrayLength, String status) {}
record Cell(String sampleId, String sampleLabel, String policyId, String policyLabel,
String outcome, String observedType, Object observedValue,
String exceptionType, List<Callback> callbacks) {}
static final class Trace implements ObjectInputFilter {
final ObjectInputFilter delegate;
final List<Callback> callbacks = new ArrayList<>();
Trace(ObjectInputFilter delegate) { this.delegate = delegate; }
@Override public Status checkInput(FilterInfo info) {
Status status = delegate.checkInput(info);
Class<?> type = info.serialClass();
callbacks.add(new Callback(type == null ? null : type.getSimpleName(),
info.arrayLength(), status.name()));
return status;
}
}
private static void check(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
checks++;
}
private static byte[] encodeOwn(Serializable value) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(bytes)) { output.writeObject(value); }
return bytes.toByteArray();
}
private static Object valueOf(Object object) {
if (object instanceof Allowed a) return a.value;
if (object instanceof Other o) return o.value;
if (object instanceof int[] array) return array.clone();
if (object instanceof String text) return text;
throw new AssertionError("unexpected observed type");
}
private static boolean sameValue(Object actual, Object expected) {
if (actual instanceof int[] a && expected instanceof int[] e) return Arrays.equals(a, e);
return actual.equals(expected);
}
private static Cell observe(Sample sample, Policy policy) throws Exception {
// Every cell has a new byte stream and Trace; no callbacks leak from other cells.
byte[] bytes = encodeOwn(sample.value());
Trace trace = new Trace(policy.filter());
try (ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
input.setObjectInputFilter(trace);
check(input.getObjectInputFilter() == trace,
"requested stream filter was replaced: " + sample.id() + "/" + policy.id());
try {
Object restored = input.readObject();
return new Cell(sample.id(), sample.label(), policy.id(), policy.label(),
"read", restored.getClass().getSimpleName(), valueOf(restored), null,
List.copyOf(trace.callbacks));
} catch (InvalidClassException rejected) {
return new Cell(sample.id(), sample.label(), policy.id(), policy.label(),
"rejected", null, null, rejected.getClass().getSimpleName(),
List.copyOf(trace.callbacks));
}
}
}
private static String quote(String text) {
if (text == null) return "null";
StringBuilder out = new StringBuilder("\"");
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
switch (ch) {
case '"' -> out.append("\\\"");
case '\\' -> out.append("\\\\");
case '\b' -> out.append("\\b");
case '\f' -> out.append("\\f");
case '\n' -> out.append("\\n");
case '\r' -> out.append("\\r");
case '\t' -> out.append("\\t");
default -> {
if (ch < 0x20) {
String hex = Integer.toHexString(ch);
out.append("\\u").append("0".repeat(4 - hex.length())).append(hex);
} else out.append(ch);
}
}
}
return out.append('"').toString();
}
private static String jsonValue(Object value) {
if (value == null) return "null";
if (value instanceof Integer number) return number.toString();
if (value instanceof String text) return quote(text);
if (value instanceof int[] array) {
StringBuilder out = new StringBuilder("[");
for (int i = 0; i < array.length; i++) { if (i > 0) out.append(','); out.append(array[i]); }
return out.append(']').toString();
}
throw new AssertionError("unexpected JSON value");
}
private static String render(List<Cell> cells) {
StringBuilder out = new StringBuilder();
out.append("{\n \"schemaVersion\": 1,\n")
.append(" \"recordType\": \"fixed-actual-java-observations\",\n")
.append(" \"sampleCount\": 6,\n \"policyCount\": 4,\n \"cellCount\": 24,\n")
.append(" \"outcomeChecks\": ").append(outcomeChecks).append(",\n")
.append(" \"checks\": ").append(checks).append(",\n \"cells\": [\n");
for (int i = 0; i < cells.size(); i++) {
Cell cell = cells.get(i);
if (i > 0) out.append(",\n");
out.append(" {\"sampleId\":").append(quote(cell.sampleId()))
.append(",\"sampleLabel\":").append(quote(cell.sampleLabel()))
.append(",\"policyId\":").append(quote(cell.policyId()))
.append(",\"policyLabel\":").append(quote(cell.policyLabel()))
.append(",\"outcome\":").append(quote(cell.outcome()))
.append(",\"observedType\":").append(quote(cell.observedType()))
.append(",\"observedValue\":").append(jsonValue(cell.observedValue()))
.append(",\"exceptionType\":").append(quote(cell.exceptionType()))
.append(",\"callbacks\":[");
for (int j = 0; j < cell.callbacks().size(); j++) {
if (j > 0) out.append(',');
Callback callback = cell.callbacks().get(j);
out.append("{\"class\":").append(quote(callback.className()))
.append(",\"arrayLength\":").append(callback.arrayLength())
.append(",\"status\":").append(quote(callback.status())).append('}');
}
out.append("]}");
}
return out.append("\n ]\n}").toString();
}
public static void main(String[] args) throws Exception {
List<Sample> samples = List.of(
new Sample("allowed-9", "Allowed(9)", new Allowed(9)),
new Sample("allowed-negative9", "Allowed(-9)", new Allowed(-9)),
new Sample("other-7", "Other(7)", new Other(7)),
new Sample("int-array-3", "int[3] = [1,2,3]", new int[] {1, 2, 3}),
new Sample("int-array-5", "int[5] = [1,2,3,4,5]", new int[] {1, 2, 3, 4, 5}),
new Sample("direct-string", "String own-text", "own-text"));
ObjectInputFilter loose = ObjectInputFilter.allowFilter(
type -> type == Allowed.class, ObjectInputFilter.Status.UNDECIDED);
ObjectInputFilter strict = ObjectInputFilter.rejectUndecidedClass(loose);
ObjectInputFilter maxArray4 = info -> info.arrayLength() > 4
? ObjectInputFilter.Status.REJECTED : ObjectInputFilter.Status.UNDECIDED;
List<Policy> policies = List.of(
new Policy("loose", "Allowed -> ALLOWED; others -> UNDECIDED", loose),
new Policy("strict", "rejectUndecidedClass(loose)", strict),
new Policy("max-array-4", "arrayLength > 4 -> REJECTED; otherwise UNDECIDED", maxArray4),
new Policy("strict-and-max-array-4", "merge(strict,maxArray4)", ObjectInputFilter.merge(strict, maxArray4)));
// Independent expected outcome table in sample order, then policy order.
String[][] expected = {
{"read", "read", "read", "read"},
{"read", "read", "read", "read"},
{"read", "rejected", "read", "rejected"},
{"read", "read", "read", "read"},
{"read", "read", "rejected", "rejected"},
{"read", "read", "read", "read"}
};
List<Cell> cells = new ArrayList<>();
for (int i = 0; i < samples.size(); i++) {
Sample sample = samples.get(i);
for (int j = 0; j < policies.size(); j++) {
Policy policy = policies.get(j);
Cell cell = observe(sample, policy);
check(cell.outcome().equals(expected[i][j]), "outcome: " + sample.id() + "/" + policy.id());
outcomeChecks++;
if (cell.outcome().equals("read")) {
check(cell.observedType().equals(sample.value().getClass().getSimpleName())
&& sameValue(cell.observedValue(), valueOf(sample.value())),
"restored type/value: " + sample.id() + "/" + policy.id());
} else {
check(cell.callbacks().stream().anyMatch(c -> c.status().equals("REJECTED")),
"rejection must have actual filter evidence");
}
if (sample.value() instanceof String) {
check(cell.callbacks().isEmpty(), "direct String must produce zero callbacks");
} else {
check(!cell.callbacks().isEmpty(), "non-String sample must have observed callbacks");
}
if (sample.value() instanceof int[] array) {
String status = cell.outcome().equals("rejected") ? "REJECTED" : "UNDECIDED";
check(cell.callbacks().stream().anyMatch(c -> "int[]".equals(c.className())
&& c.arrayLength() == array.length && c.status().equals(status)),
"actual array length and status must be recorded");
}
cells.add(cell);
}
}
check(cells.size() == 24 && outcomeChecks == 24, "complete six by four matrix");
System.out.println(render(cells));
}
}
怎样运行:在源码所在目录,先执行 javac -encoding UTF-8 --release 17 FilterMatrixDemo.java,再执行 java -cp . FilterMatrixDemo。已在Windows、JDK17.0.20.1/21.0.12独立编译运行;下方为两版一致的实际JSON输出。
运行后对照结果
{
"schemaVersion": 1,
"recordType": "fixed-actual-java-observations",
"sampleCount": 6,
"policyCount": 4,
"cellCount": 24,
"outcomeChecks": 24,
"checks": 105,
"cells": [
{"sampleId":"allowed-9","sampleLabel":"Allowed(9)","policyId":"loose","policyLabel":"Allowed -> ALLOWED; others -> UNDECIDED","outcome":"read","observedType":"Allowed","observedValue":9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"ALLOWED"}]},
{"sampleId":"allowed-9","sampleLabel":"Allowed(9)","policyId":"strict","policyLabel":"rejectUndecidedClass(loose)","outcome":"read","observedType":"Allowed","observedValue":9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"ALLOWED"}]},
{"sampleId":"allowed-9","sampleLabel":"Allowed(9)","policyId":"max-array-4","policyLabel":"arrayLength > 4 -> REJECTED; otherwise UNDECIDED","outcome":"read","observedType":"Allowed","observedValue":9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"UNDECIDED"}]},
{"sampleId":"allowed-9","sampleLabel":"Allowed(9)","policyId":"strict-and-max-array-4","policyLabel":"merge(strict,maxArray4)","outcome":"read","observedType":"Allowed","observedValue":9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"ALLOWED"}]},
{"sampleId":"allowed-negative9","sampleLabel":"Allowed(-9)","policyId":"loose","policyLabel":"Allowed -> ALLOWED; others -> UNDECIDED","outcome":"read","observedType":"Allowed","observedValue":-9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"ALLOWED"}]},
{"sampleId":"allowed-negative9","sampleLabel":"Allowed(-9)","policyId":"strict","policyLabel":"rejectUndecidedClass(loose)","outcome":"read","observedType":"Allowed","observedValue":-9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"ALLOWED"}]},
{"sampleId":"allowed-negative9","sampleLabel":"Allowed(-9)","policyId":"max-array-4","policyLabel":"arrayLength > 4 -> REJECTED; otherwise UNDECIDED","outcome":"read","observedType":"Allowed","observedValue":-9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"UNDECIDED"}]},
{"sampleId":"allowed-negative9","sampleLabel":"Allowed(-9)","policyId":"strict-and-max-array-4","policyLabel":"merge(strict,maxArray4)","outcome":"read","observedType":"Allowed","observedValue":-9,"exceptionType":null,"callbacks":[{"class":"Allowed","arrayLength":-1,"status":"ALLOWED"}]},
{"sampleId":"other-7","sampleLabel":"Other(7)","policyId":"loose","policyLabel":"Allowed -> ALLOWED; others -> UNDECIDED","outcome":"read","observedType":"Other","observedValue":7,"exceptionType":null,"callbacks":[{"class":"Other","arrayLength":-1,"status":"UNDECIDED"}]},
{"sampleId":"other-7","sampleLabel":"Other(7)","policyId":"strict","policyLabel":"rejectUndecidedClass(loose)","outcome":"rejected","observedType":null,"observedValue":null,"exceptionType":"InvalidClassException","callbacks":[{"class":"Other","arrayLength":-1,"status":"REJECTED"}]},
{"sampleId":"other-7","sampleLabel":"Other(7)","policyId":"max-array-4","policyLabel":"arrayLength > 4 -> REJECTED; otherwise UNDECIDED","outcome":"read","observedType":"Other","observedValue":7,"exceptionType":null,"callbacks":[{"class":"Other","arrayLength":-1,"status":"UNDECIDED"}]},
{"sampleId":"other-7","sampleLabel":"Other(7)","policyId":"strict-and-max-array-4","policyLabel":"merge(strict,maxArray4)","outcome":"rejected","observedType":null,"observedValue":null,"exceptionType":"InvalidClassException","callbacks":[{"class":"Other","arrayLength":-1,"status":"REJECTED"}]},
{"sampleId":"int-array-3","sampleLabel":"int[3] = [1,2,3]","policyId":"loose","policyLabel":"Allowed -> ALLOWED; others -> UNDECIDED","outcome":"read","observedType":"int[]","observedValue":[1,2,3],"exceptionType":null,"callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":3,"status":"UNDECIDED"}]},
{"sampleId":"int-array-3","sampleLabel":"int[3] = [1,2,3]","policyId":"strict","policyLabel":"rejectUndecidedClass(loose)","outcome":"read","observedType":"int[]","observedValue":[1,2,3],"exceptionType":null,"callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":3,"status":"UNDECIDED"}]},
{"sampleId":"int-array-3","sampleLabel":"int[3] = [1,2,3]","policyId":"max-array-4","policyLabel":"arrayLength > 4 -> REJECTED; otherwise UNDECIDED","outcome":"read","observedType":"int[]","observedValue":[1,2,3],"exceptionType":null,"callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":3,"status":"UNDECIDED"}]},
{"sampleId":"int-array-3","sampleLabel":"int[3] = [1,2,3]","policyId":"strict-and-max-array-4","policyLabel":"merge(strict,maxArray4)","outcome":"read","observedType":"int[]","observedValue":[1,2,3],"exceptionType":null,"callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":3,"status":"UNDECIDED"}]},
{"sampleId":"int-array-5","sampleLabel":"int[5] = [1,2,3,4,5]","policyId":"loose","policyLabel":"Allowed -> ALLOWED; others -> UNDECIDED","outcome":"read","observedType":"int[]","observedValue":[1,2,3,4,5],"exceptionType":null,"callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":5,"status":"UNDECIDED"}]},
{"sampleId":"int-array-5","sampleLabel":"int[5] = [1,2,3,4,5]","policyId":"strict","policyLabel":"rejectUndecidedClass(loose)","outcome":"read","observedType":"int[]","observedValue":[1,2,3,4,5],"exceptionType":null,"callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":5,"status":"UNDECIDED"}]},
{"sampleId":"int-array-5","sampleLabel":"int[5] = [1,2,3,4,5]","policyId":"max-array-4","policyLabel":"arrayLength > 4 -> REJECTED; otherwise UNDECIDED","outcome":"rejected","observedType":null,"observedValue":null,"exceptionType":"InvalidClassException","callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":5,"status":"REJECTED"}]},
{"sampleId":"int-array-5","sampleLabel":"int[5] = [1,2,3,4,5]","policyId":"strict-and-max-array-4","policyLabel":"merge(strict,maxArray4)","outcome":"rejected","observedType":null,"observedValue":null,"exceptionType":"InvalidClassException","callbacks":[{"class":"int[]","arrayLength":-1,"status":"UNDECIDED"},{"class":"int[]","arrayLength":5,"status":"REJECTED"}]},
{"sampleId":"direct-string","sampleLabel":"String own-text","policyId":"loose","policyLabel":"Allowed -> ALLOWED; others -> UNDECIDED","outcome":"read","observedType":"String","observedValue":"own-text","exceptionType":null,"callbacks":[]},
{"sampleId":"direct-string","sampleLabel":"String own-text","policyId":"strict","policyLabel":"rejectUndecidedClass(loose)","outcome":"read","observedType":"String","observedValue":"own-text","exceptionType":null,"callbacks":[]},
{"sampleId":"direct-string","sampleLabel":"String own-text","policyId":"max-array-4","policyLabel":"arrayLength > 4 -> REJECTED; otherwise UNDECIDED","outcome":"read","observedType":"String","observedValue":"own-text","exceptionType":null,"callbacks":[]},
{"sampleId":"direct-string","sampleLabel":"String own-text","policyId":"strict-and-max-array-4","policyLabel":"merge(strict,maxArray4)","outcome":"read","observedType":"String","observedValue":"own-text","exceptionType":null,"callbacks":[]}
]
}记住这三点
- UNDECIDED 是未作判断,普通类在没有拒绝结果时仍可继续恢复。
serialClass为null的指标回调需要单独处理,不能与过滤器返回null混淆。- 数组上限、直接
String路径和业务字段各有检查边界,应分别记录证据。
别把例子用过头
- 只写一个允许分支,却把其余 UNDECIDED 理解为全部拒绝。
- 在判断
serialClass是否为空之前访问它的方法。 - 把
arrayLength=-1 当成空数组,或遗漏长度恰好等于上限的样本。 - 以为
rejectUndecidedClass会拒绝全部未决数组,或拒绝回调一定会检查字符串。
自己试一小步
把数组限制改为最多两个元素,并为 Allowed 的整数定义独立业务规则,怎样验证两层检查?
- 复制为新的单文件实验,先列出长度零、二、三的数组样本及预测。
- 修改资源过滤规则,在 Trace 中保存实际数组长度与状态,为每个边界写断言。
- 为读回 Allowed 后的 value 增加单独的业务检查,规则是零到十(含端点),设计负一、零、十、十一的样本。
- 分别保存类型过滤结果、对象是否完成读回和业务检查结果,不用一个笼统的成功标志替代。
做到这些,就算完成
- 长度零和二被接受,长度三被拒绝;对应回调长度和状态可以从记录核对。
- 负一和十一即使类型过滤允许,仍由独立业务检查拒绝;零与十通过。
- 保留
serialClass为null的分支,并用共享引用样本验证不会误报。 - 全部字节由当前实验自己产生,保存 JDK 版本、实际输出和每项断言结果。
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- ObjectInputFilter:未决状态与组合方法 (新标签页)↗
核对
allowFilter、rejectUndecidedClass对普通类、空类事件与基本类型数组的区别。 - Java 17 序列化过滤指南 (新标签页)↗
核对允许列表、资源限制和直接编码
String的过滤例外。 - ObjectInputStream:过滤器调用时机 (新标签页)↗
核对读取前设置一次过滤器、拒绝异常及引用、数组、字符串的回调规则。
- FilterInfo:空类信息与数组长度 (新标签页)↗
核对
null类事件、arrayLength的 -1/非负长度,以及深度、引用和字节计数。