JacksonDSL

2025-12-23, 星期二, 13:49

DevJava

一些第三方接口的请求参数可能是结构复杂的 JSON:

  1. 多层嵌套
  2. 包含列表
  3. 需要动态调整结构

例如要发送浙政钉 OA 工作通知,你需要组装这样一个结构:

{
  "msgtype" : "oa",
  "oa" : {
    "message_url" : "https://bing.com",
    "pc_message_url" : "https://baidu.com",
    "head" : {
      "bgcolor" : "0000FFFF",
      "text" : "头部标题"
    },
    "body" : {
      "title" : "正文标题",
      "form" : [ {
        "key" : "表单字段一",
        "value" : "字段值一"
      }, {
        "key" : "表单字段二",
        "value" : "字段值二"
      } ],
      "rich" : {
        "num" : "9.15",
        "unit" : "镑"
      },
      "content" : "Lorem ipsum dolor sit amet",
      "image" : "@ImageXXASXFD",
      "file_count" : 3
    }
  }
}

如果使用 Jackson 将请求体转换为 JSON,至少需要定义一系列 POJO 类(如 OAMessageHeadPropertiesBodyPropertiesRichMessageProperties 等)。将它们设计为内部类或许能改善可读性,但可能还需要额外提供 OAMessageBuilder 来帮助调用者组装请求体。

如果使用 JDK 14 之后的 Text Block 和模板字符串,开发者需要自行确保拼接结果是合法的 JSON。此外,处理列表时需要将列表元素做成子模板。由于输出结果是字符串,日志记录时也不便于格式化缩进和换行。

直接组装(使用 Map、Fastjson 的 JSONObject 或 Jackson 的 ObjectNode)则结构不够直观:

ObjectNode root = mapper.createObjectNode();
root.put("msgtype", "oa");

ObjectNode oaNode = root.putObject("oa");
ObjectNode headNode = oaNode.putObject("head");
headNode.put("message_url", "https://bing.com");
// ...

本文提供一种基于 Jackson 的声明式 JSON 组装方法,通过简洁的 DSL 语法让代码结构与 JSON 结构保持一致。上述 JSON 结构可表达为:

o(
    p("msgtype", "oa"),
    p("oa", o(
        p("message_url", "https://bing.com"),
        p("pc_message_url", "https://baidu.com"),
        p("head", o(
            p("bgcolor", "0000FFFF"),
            p("text", "头部标题")
        )),
        p("body", o(
            p("title", "正文标题"),
            p("form", a(
                o(p("key", "表单字段一"), p("value", "字段值一")),
                o(p("key", "表单字段二"), p("value", "字段值二"))
            )),
            p("rich", o(
                p("num", "9.15"),
                p("unit", "镑")
            )),
            p("content", "Lorem ipsum dolor sit amet"),
            p("image", "@ImageXXASXFD"),
            p("file_count", 3)
        ))
    ))
);

其中:

  • o(...) — 创建一个 JSON 对象(Object)
  • p(key, value) — 插入一个键值对(Property)
  • a(...) — 创建一个 JSON 数组(Array)

条件、函数式参数与惰性求值

条件结构

动态组装 JSON 时,常需根据条件选择性地添加属性。p 方法提供了一个带条件的重载版本,当 conditionfalse 时,该属性会被跳过。

p(boolean condition, String key, Object value)

以如下需求为例,MessageType 可以是 linkmarkdown,对应的负载结构不同。

# 发送链接消息
{
    "type": "link",
    "content": {
        "message_url": "%s",
        "text": "%s"
    }
}

# 发送 Markdown 消息
{
    "type":"markdown",
    "content":"%s"
}

可以通过多个带条件的 p() 实现:

o(
    p("type", type),
    p(type.equals("link"), "content", o(
                                        p("message_url", url),
                                        p("text", text)
    )),
    p(type.equals("markdown"), "content", text)
)

惰性求值

但这种写法存在一个问题:

p(Objects.nonNull(btns), "buttons", a(btns.stream().map(Button::toJSON).toList()));

虽然意图是 btns 为 NULL 时跳过该属性,由于 Java 的求值顺序,btns.stream().map(Button::toJSON).toList() 会在方法调用前先被计算,从而抛出空指针异常。

为解决这个问题,p 方法支持惰性求值 —— value 参数可以是 Supplier<?> 类型,只有当 conditiontrue 时才会执行:

p(Objects.nonNull(btns), "buttons", () -> a(btns.stream().map().toList()));

上一节的示例可改写为:

o(
  p("type", type),
  p("content", () -> {
    switch (type) {
      case "link":
        return o(
          p("message_url", url),
          p("text", text)
        );
      case "markdown": return text;
      default:throw new IllegalStateException("Unexpected value: " + type);
    }
  })
);

简化集合

如果 valueCollection 类型(如 ListSet),将自动转换为 JSON 数组,无需使用 a() 包装:

List<String> tags = List.of("java", "jackson", "dsl");
p("tags", tags)  // 等价于 p("tags", a("java", "jackson", "dsl"))

源代码