首页 · ← SuperJ 手册 中文|EN

JSON:无垃圾的解析与构建

sj.json 是一个手写的增量式 JSON 解析器,外加一个配套的流式构建器 — 没有 derive 宏,没有反射,没有你不曾要求的中间对象图。本教程把这两半都过一遍:从最简单的 API 讲起,最后落到任何热路径(一个请求处理器、每 tick 一次的 SEDA 事件、任何在循环里被调用的东西)都该使用的零垃圾模式。每段代码都改编自 SDK 附带的演示(demo/sj/demo/JsonDemo.sj、demo/sj/demo/JsonBuilderDemo.sj),照原样即可编译运行。

1. 解析 — 一次性 API

读一个 JSON 文档最简单的方式:

import sj.json.JsonParser;
import sj.json.JsonNode;

byte[] json = "{\"name\":\"SuperJ\",\"versions\":[1,2,3],\"fast\":true}".getBytes();
JsonNode doc = JsonParser.parse(json);

if (doc.isObject()) {
    System.out.println("top-level object, " + doc.childCount() + " fields");
}

JsonNode 是解析后文档的一张扁平"tape"视图 — isObject()、isArray()、isString()、isLong()、isDouble()、isBoolean()、isNull() 告诉你一个节点是什么;asLong()、asDouble()、asBoolean()、asString() 读取它的值。

对象与数组按子节点的链式序列遍历,而不是按索引:

JsonNode key = doc.firstChild();
while (key != null && key.isString()) {
    JsonNode val = key.next();
    System.out.println(key.asString() + " = " + describe(val));
    key = val.next();               // advance past the value to the next key
}

对数组而言,firstChild()/nextSibling() 直接遍历元素(中间没有 key)。

查找 key,以及重复 key

at(key) 在一个对象的成员里查找并返回匹配的值,没有则返回 null。它是从对象第一个成员开始的线性扫描,不是索引 — 对象在 tape 上就是一个序列,没有任何东西对它做哈希。

如果同一个 key 在对象里出现两次,后写的赢。

JsonNode n = JsonParser.parse("{\"k\":1,\"k\":2}".getBytes());
n.at("k").asLong();          // 2
n.childCount();              // 2 — both members are still on the tape

RFC 8259 §4 说名称应当唯一,并把重复名称的含义留给实现自行决定。后写赢(last-wins)正是一个构建 map 的解析器给你的结果,因为后插入的会覆盖先插入的,所以这也是从 Jackson、Gson 或 serde_json 过来的调用者所期望的(#4325)。

没有任何东西被丢弃:解析保留每一个成员,childCount() 把它们全部计数,firstChild()/next() 的遍历两个都能看到。只有 at() 会挑出赢家。其结果是 at() 无法在第一个匹配处停下 — 要排除后面还有重复的可能,就得看完剩下的每个 key — 所以一次查找的代价是走完整个对象。如果你要从一个大对象里读出多个 key,自己遍历一次,而不是对每个 key 都调用一次 at();反正 at() 每次都从头开始。

这个 API 适合一次性的解析。JsonParser.parse(...) 会分配一个新的 JsonParser,而每次 firstChild()/next()/nextSibling()/at() 调用都会分配一个全新的 JsonNode。在热循环里这些会累积起来。

2. 解析 — 零垃圾模式

sj.json 的增量式设计(设计笔记见 doc/jsonDesign.md)意味着一个 JsonParser 会在多次调用之间保留它的 tape/string/double 缓冲区 — 此后剩下的唯一分配就是返回给你的那些 JsonNode 对象。如果你自己提供节点,让解析器(以及节点本身)原地重绑它们而不是返回新对象,这些分配也会消失:

JsonParser parser = new JsonParser();   // allocated once, outside the loop
JsonNode root = new JsonNode();         // unbound "out-parameter" node, reused every parse
JsonNode field = new JsonNode();        // unbound out-parameter, reused every navigation step

while (moreMessages()) {
    parser.parseInto(root, nextMessage());        // rebinds `root`; allocates nothing
    if (root.atInto(field, "name")) {              // rebinds `field`; allocates nothing
        boolean isBob = field.stringEquals("bob"); // byte-for-byte compare, no String
        byte[] name = field.asStringBytes();       // only allocate when you need to keep it
    }
}

atInto 每次调用仍然从对象开头线性遍历 — 它是查找,不是持久化索引 — 但遍历本身以及逐 key 的比较不做任何分配。

3. 构建 — 链式对象/数组构造

JsonBuilder 在你调用它的同时就把 JSON 直接写进字节缓冲区 — 没有"先建一棵中间树再序列化"这回事。

import sj.json.JsonBuilder;
import sj.util.ByteArray;

JsonBuilder jb = new JsonBuilder(new ByteArray(256));
ByteArray doc = jb.build(jb.object()
        .add("name", "SuperJ")
        .add("stars", 5)
        .add(jb.object("author")
                .add("org", "SuperJ")
                .add("active", true))
        .add(jb.array("tags")
                .add("json")
                .add("zero-garbage")));

System.out.println(doc.toString());
// {"name":"SuperJ","stars":5,"author":{"org":"SuperJ","active":true},"tags":["json","zero-garbage"]}

4. 构建 — 零垃圾模式

把 JsonBuilder 和它的后备 ByteArray 一次性分配好,放在循环外。build() 每次调用都会清空并重新翻转同一个缓冲区 — 每条消息不会有任何新分配:

ByteArray out = new ByteArray(256);
JsonBuilder jb = new JsonBuilder(out);   // once, outside the loop

while (moreMessages()) {
    ByteArray msg = jb.build(jb.object().add("seq", i).add("name", names[i]));
    send(msg);   // fully consume before the next build() call
}

两条规则保证这样做是安全的:

  1. 绝不要每条消息都 new JsonBuilder(...)。 构造函数会在输出缓冲区之外再分配两个 64KB 的内部暂存缓冲区(buf、vbuf),用于名字/值的 staging。JsonBuilder implements Reusable 正是为此 — 构造一次,多次调用 build()。
  2. 返回的 ByteArray 就是 builder 自己的输出缓冲区,只在同一 builder 的下一次 build() 调用之前有效。把它写出去(写到 socket、日志,或为会拷贝的调用者 toString() 一份)之后再去构建下一条消息 — 不要跨迭代持有它。

如果你要自己把多个 JSON 片段拼进一个更大的缓冲区(而不是每次 build() 调用构建一个文档),用 new JsonBuilder(out, false) 构造 — flipping = false 会跳过自动翻转,共享缓冲区何时转为读取由你控制。

5. 小结

本模式所从属的通用分配阶梯纪律见 内存管理最佳实践,GC-free 审查清单见 编码规范。

SuperJ — manual · generated from json.md at pack time · Powered by superJ — this site is served by superj_web 中文|EN