JacksonDSL

2025-11-12, 星期三, 14:15

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

  1. 有多层嵌套
  2. 有列表参数
  3. 需要动态变更结构

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

{
  "msgtype" : "action_card",
  "action_card" : {
    "title" : "工作通知",
    "markdown" : "内容其实不支持 markdown",
    "btn_orientation" : 1,
    "btn_json_list" : [{
      "title": "查看详情",
      "action_pc_url": "https://www.baidu.com/",
      "action_mobile_url": "https://www.baidu.com/"
    }]
  }
}

如果使用 Jackson 将请求体转换为 JSON,则至少需要设计三个 POJO 类(SendMessageRequestActionCardPropertiesActionButtonProperties),也许还需要提供 SendMessageRequestBuilder 类帮助服务调用者组装请求体。

如果使用 JDK 14 之后的 Text Block 和模板字符串,开发者需要自行确保拼接结果是个合法的 JSON 结构。此外涉及到列表处理时,列表成员也需要做成子模板。由于这样的输出结果是个字符串,在日志留存时并不会特别处理缩进和换行。

直接组装的话(Map、Fastjson 的 JSONObject 或 Jackson 的 ObjectNode),结构显然不够直观:

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

ObjectNode actionCard = root.putObject("action_card");
data.put("title", "工作通知");
// ...

本文提供了一种声明式的 JSON 组装方法,上述 JSON 结构可被表达为:

o(
  p("msgtype", "action_card"),
  p("action_card", o(
          p("title", "工作通知"),
          p("markdown", "内容其实不支持 markdown"),
          p("btn_orientation", 1),
          p("btn_json_list", a(
                  o(
                     p("title", "查看详情"),
                     p("action_pc_url", "https://baidu.com/"),
                     p("action_mobile_url", "https://baidu.com/")
                  )
          )))));

其中 o()代表一个 Node 结构的开始,p()代表插入一个属性键值对,a()表示开始一种 Array 结构。

由于所有输入其实都是函数参数,要支持条件结构,例如下面演示的那样:

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

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

可以在传递参数时声明并立即执行 lambda 方法:

private ObjectNode makeMessage(String messageType, String text, String url) {
    return o(
            p("type", messageType),
            ((Supplier<Map.Entry>) () -> switch (messageType) {
                case "link" -> p("link", o(
                        p("messageUrl", url),
                        p("text", text)
                ));
                case "markdown" -> p("text", text);
                default -> throw new IllegalStateException("Unexpected value: " + messageType);
            }).get());
}

暂不支持将函数本身作为参数传递,不然就可以实现惰性求值了。

实现代码如下:

public class JacksonDSL {

    private static ObjectMapper mapper = new ObjectMapper();

    /**
     * 替换为自定义的 ObjectMapper 实例,实现某些特殊转换需求,影响全部 DSL 方法
     */
    public static void setObjectMapper(ObjectMapper mapper) {
        JacksonDSL.mapper = mapper;
    }

    // p("key", value)
    public static Map.Entry<String, Object> p(String key, Object value) {
        return new AbstractMap.SimpleEntry<>(key, value);
    }

    // o(p(...), p(...))
    @SafeVarargs
    public static ObjectNode o(Map.Entry<String, Object>... entries) {
        ObjectNode node = mapper.createObjectNode();
        for (Map.Entry<String, Object> e : entries) {
            Object v = e.getValue();
            if (v instanceof Map.Entry) {
                throw new IllegalArgumentException("Use o() or a(), not nested p().");
            } else if (v instanceof ObjectNode on) {
                node.set(e.getKey(), on);
            } else if (v instanceof ArrayNode an) {
                node.set(e.getKey(), an);
            } else if (v instanceof JsonNode jn) {
                node.set(e.getKey(), jn);
            } else if (v instanceof String s) {
                node.put(e.getKey(), s);
            } else if (v instanceof Integer i) {
                node.put(e.getKey(), i);
            } else if (v instanceof Long l) {
                node.put(e.getKey(), l);
            } else if (v instanceof Double d) {
                node.put(e.getKey(), d);
            } else if (v instanceof Boolean b) {
                node.put(e.getKey(), b);
            } else if (v == null) {
                node.putNull(e.getKey());
            } else {
                // fallback: convert POJO or Map/List into JsonNode
                node.set(e.getKey(), mapper.valueToTree(v));
            }
        }
        return node;
    }

    // a(o(...), o(...))
    public static ArrayNode a(Object... values) {
        ArrayNode arr = mapper.createArrayNode();
        for (Object v : values) {
            if (v instanceof ObjectNode on) {
                arr.add(on);
            } else if (v instanceof ArrayNode an) {
                arr.add(an);
            } else if (v instanceof JsonNode jn) {
                arr.add(jn);
            } else if (v instanceof String s) {
                arr.add(s);
            } else if (v instanceof Integer i) {
                arr.add(i);
            } else if (v instanceof Long l) {
                arr.add(l);
            } else if (v instanceof Double d) {
                arr.add(d);
            } else if (v instanceof Boolean b) {
                arr.add(b);
            } else if (v == null) {
                arr.addNull();
            } else {
                arr.add(mapper.valueToTree(v));
            }
        }
        return arr;
    }

    private JacksonDSL() {
        throw new UnsupportedOperationException("JacksonDSL is a utility class and cannot be instantiated.");
    }
}