最近排查问题的时候,顺便给 fastjson2 提了个 PR #7641,maintainer review 后提了点问题。
1. review 说了什么
code review 主要提了两个点。
write() 文本路径漏了。 第一版修了 writeJSONB(),但 JSON.toJSONString(set, ReferenceDetection) 也会写出不可解析的 $ref。实测输出类似:外层根节点是 Set 序列化出来的数组,
$.codes 读回来找不到,字段就是 null。
单元素循环引用可能炸。 如果继续用
suppressElementRefDetect 压掉子 writer 的引用检测,单元素 HashSet 里 bean 指回自己、或者指回外层 Set,递归可能停不下来。轻一点 level too large,重一点 StackOverflowError。2. 先补测试,再动代码
接收到CR建议后,不要急于直接去修改代码,而是要先根据提到的问题点,设计出合理的用例场景。
- 文本路径:
JSON.toJSONString()往返
- 单元素循环:自引用、多跳循环、指回外层 Set
TreeSet:SortedSet在refDetect判断里走另一条路径,只测HashSet不够
- 回归和边界:
ArrayList、空集合、单元素集合、不开ReferenceDetection
按照问题点将涉及的要点列出来,把对于的测试用例补充出来。
3. 第一次修复:看着对,测不过
我第一反应很直接:把 JSONB 里已有的
suppressElementRefDetect 挪到 write()。思路是写非 List 集合元素时,临时压掉子 writer 的引用检测。第二个、第三个元素里的
codes 就不会写成 $ref,而是内联写完整对象。听上去合理对吧?
跑测试,15/18 通过。共享引用那批好了,3 个单元素循环测试挂了。

4. 读源码:两层机制在打架
重新看
ObjectWriterImplCollection,问题不在「要不要开引用检测」,而是两层机制冲突了。机制 | 干什么 | 单元素 Set |
集合层 refDetect | 写元素前 setPath(i, item) 注册路径 | 仍然开着 |
suppressElementRefDetect | 清 context 级 ReferenceDetection | 非 List 一律 suppress |
外层觉得「我已经在做循环保护了」,内层却被关掉了真正终止递归的能力。无限递归就这么来的。
根因也清楚了:非 List 集合里 sibling 共享同一张 refs 表。前一个元素注册出来的
$.codes,泄漏给了后一个元素。路径本身在 Set 根下就不可解析。所以不该关功能,该给 refs 划边界。

5. 换方案:saveReferences / restoreReferences
最终修法是给
JSONWriter 加两个很小的 API:进入非 List 集合时,先保存一份 ancestor refs 快照。每写一个 element 前,恢复到这份快照。
- 元素内部:
ReferenceDetection仍开着,循环引用还能靠$ref停住
- sibling 之间:前一个元素注册的
codes -> $.codes不会带到下一个
writeJSONB()和write()两条路径同一套逻辑
比
suppressElementRefDetect 绕一点,但语义准。前者像拉电闸,后者像把线路接对。
6. 性能:问题的修复是有代价的
每个 element 写入前恢复一次 refs 快照,理论上会有开销。元素多、refs 表大的时候,不能拍脑袋说没有。
不过触发条件本来就不太常见:非 List 集合 + 开启
ReferenceDetection + 元素内部 sibling 共享引用。正常小数量对象下,预期影响不会太明显。真要知道对性能影响多大,得跑一轮 benchmark 才知道。
- 作者:Yibin
- 链接:https://yibin.dev/article/39460b50-99a4-8067-baf0-fbb3b03ff1f8
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章







