一些第三方接口的请求参数可能是结构复杂的 JSON:
- 多层嵌套
- 包含列表
- 需要动态调整结构
例如要发送浙政钉 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 类(如 OAMessage、HeadProperties、BodyProperties、RichMessageProperties 等)。将它们设计为内部类或许能改善可读性,但可能还需要额外提供 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 方法提供了一个带条件的重载版本,当 condition 为 false 时,该属性会被跳过。
p(boolean condition, String key, Object value)
以如下需求为例,MessageType 可以是 link 或 markdown,对应的负载结构不同。
# 发送链接消息
{
"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<?> 类型,只有当 condition 为 true 时才会执行:
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);
}
})
);
简化集合
如果 value 是 Collection 类型(如 List、Set),将自动转换为 JSON 数组,无需使用 a() 包装:
List<String> tags = List.of("java", "jackson", "dsl");
p("tags", tags) // 等价于 p("tags", a("java", "jackson", "dsl"))