1. 概述
本文将深入探讨如何在 Jackson 中操作 树模型节点(Tree Model Nodes)。
我们将使用 JsonNode 来进行各种转换操作,包括添加、修改和删除节点。
2. 创建节点
第一步是通过默认构造函数实例化一个 ObjectMapper 对象:
ObjectMapper mapper = new ObjectMapper();
⚠️ 注意:ObjectMapper 的创建开销较大,建议在多个操作中复用同一个实例。
有了 ObjectMapper 后,可以通过以下三种方式来创建树节点。
2.1. 从零开始构造节点
这是最常见的创建空节点的方式:
JsonNode node = mapper.createObjectNode();
或者也可以通过 JsonNodeFactory 来创建:
JsonNode node = JsonNodeFactory.instance.objectNode();
2.2. 从 JSON 源解析
该方法在 Jackson – Marshall String to JsonNode 一文中有详细介绍,可前往查阅。
2.3. 从对象转换而来
通过调用 ObjectMapper 的 valueToTree(Object fromValue)
方法,可以将一个 Java 对象转换为 JsonNode:
JsonNode node = mapper.valueToTree(fromValue);
convertValue
API 也有类似作用:
JsonNode node = mapper.convertValue(fromValue, JsonNode.class);
来看一个实际例子。
假设我们有一个类 NodeBean:
public class NodeBean {
private int id;
private String name;
public NodeBean() {
}
public NodeBean(int id, String name) {
this.id = id;
this.name = name;
}
// 标准的 getter 和 setter 省略
}
下面的测试验证转换是否正确:
@Test
public void givenAnObject_whenConvertingIntoNode_thenCorrect() {
NodeBean fromValue = new NodeBean(2016, "baeldung.com");
JsonNode node = mapper.valueToTree(fromValue);
assertEquals(2016, node.get("id").intValue());
assertEquals("baeldung.com", node.get("name").textValue());
}
3. 节点转换操作
3.1. 输出为 JSON
这是将树节点转换为 JSON 字符串的基本方法,目标可以是 File、OutputStream 或 Writer:
mapper.writeValue(destination, node);
基于第 2.3 节中定义的 NodeBean 类,下面的测试确保该方法按预期工作:
final String pathToTestFile = "node_to_json_test.json";
@Test
public void givenANode_whenModifyingIt_thenCorrect() throws IOException {
String newString = "{\"nick\": \"cowtowncoder\"}";
JsonNode newNode = mapper.readTree(newString);
JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).set("name", newNode);
assertFalse(rootNode.path("name").path("nick").isMissingNode());
assertEquals("cowtowncoder", rootNode.path("name").path("nick").textValue());
}
3.2. 转换为 Java 对象
最方便的方式是使用 treeToValue
API:
NodeBean toValue = mapper.treeToValue(node, NodeBean.class);
这与下面的写法功能一致:
NodeBean toValue = mapper.convertValue(node, NodeBean.class)
还可以通过 token 流的方式:
JsonParser parser = mapper.treeAsTokens(node);
NodeBean toValue = mapper.readValue(parser, NodeBean.class);
最后,测试一下转换过程是否正确:
@Test
public void givenANode_whenConvertingIntoAnObject_thenCorrect()
throws JsonProcessingException {
JsonNode node = mapper.createObjectNode();
((ObjectNode) node).put("id", 2016);
((ObjectNode) node).put("name", "baeldung.com");
NodeBean toValue = mapper.treeToValue(node, NodeBean.class);
assertEquals(2016, toValue.getId());
assertEquals("baeldung.com", toValue.getName());
}
4. 节点操作(增删改查)
我们使用以下 JSON 内容作为示例结构(位于 classpath 的 example.json):
{
"name": {
"first": "Tatu",
"last": "Saloranta"
},
"title": "Jackson founder",
"company": "FasterXML"
}
该 JSON 被解析成一个树模型:
public class ExampleStructure {
private static ObjectMapper mapper = new ObjectMapper();
static JsonNode getExampleRoot() throws IOException {
InputStream exampleInput =
ExampleStructure.class.getClassLoader()
.getResourceAsStream("example.json");
JsonNode rootNode = mapper.readTree(exampleInput);
return rootNode;
}
}
后续小节的操作都基于该结构。
4.1. 定位节点
在操作节点前,首先要定位到它:
JsonNode locatedNode = rootNode.path("name").path("last");
也可以使用 get
或 with
代替 path
。
如果路径未知,则需要通过迭代方式查找(见 第 5 节)。
4.2. 添加新节点
可以通过以下方式向节点添加子节点:
ObjectNode newNode = ((ObjectNode) locatedNode).put(fieldName, value);
put
有多个重载版本,支持不同类型的值。
还有其他类似方法,如:putArray
, putObject
, putPOJO
, putRawValue
, putNull
。
示例:向根节点添加一个地址结构:
"address": {
"city": "Seattle",
"state": "Washington",
"country": "United States"
}
完整测试如下:
@Test
public void givenANode_whenAddingIntoATree_thenCorrect() throws IOException {
JsonNode rootNode = ExampleStructure.getExampleRoot();
ObjectNode addedNode = ((ObjectNode) rootNode).putObject("address");
addedNode
.put("city", "Seattle")
.put("state", "Washington")
.put("country", "United States");
assertFalse(rootNode.path("address").isMissingNode());
assertEquals("Seattle", rootNode.path("address").path("city").textValue());
assertEquals("Washington", rootNode.path("address").path("state").textValue());
assertEquals(
"United States", rootNode.path("address").path("country").textValue();
}
4.3. 修改节点
通过 set(String fieldName, JsonNode value)
修改节点:
JsonNode locatedNode = locatedNode.set(fieldName, value);
也可使用 replace
或 setAll
方法。
测试:将 name
字段从对象结构替换为 nick
字段:
@Test
public void givenANode_whenModifyingIt_thenCorrect() throws IOException {
String newString = "{\"nick\": \"cowtowncoder\"}";
JsonNode newNode = mapper.readTree(newString);
JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).set("name", newNode);
assertFalse(rootNode.path("name").path("nick").isMissingNode());
assertEquals("cowtowncoder", rootNode.path("name").path("nick").textValue());
}
4.4. 删除节点
通过调用父节点的 remove(String fieldName)
删除节点:
JsonNode removedNode = locatedNode.remove(fieldName);
要删除多个字段,可使用 remove(Collection<String>)
:
ObjectNode locatedNode = locatedNode.remove(fieldNames);
如果要清空所有子节点,可使用 removeAll()
。
测试删除 company
字段:
@Test
public void givenANode_whenRemovingFromATree_thenCorrect() throws IOException {
JsonNode rootNode = ExampleStructure.getExampleRoot();
((ObjectNode) rootNode).remove("company");
assertTrue(rootNode.path("company").isMissingNode());
}
5. 遍历节点
我们将遍历 JSON 文档的所有节点,并将其格式化为 YAML。
JSON 有三种节点类型:Value、Object 和 Array。
我们通过添加一个 Array 类型字段来覆盖所有类型:
{
"name": {
"first": "Tatu",
"last": "Saloranta"
},
"title": "Jackson founder",
"company": "FasterXML",
"pets" : [
{
"type": "dog",
"number": 1
},
{
"type": "fish",
"number": 50
}
]
}
期望的 YAML 输出:
name:
first: Tatu
last: Saloranta
title: Jackson founder
company: FasterXML
pets:
- type: dog
number: 1
- type: fish
number: 50
5.1. 测试遍历
测试代码:
@Test
public void givenANodeTree_whenIteratingSubNodes_thenWeFindExpected() throws IOException {
JsonNode rootNode = ExampleStructure.getExampleRoot();
String yaml = onTest.toYaml(rootNode);
assertEquals(expectedYaml, yaml);
}
public String toYaml(JsonNode root) {
StringBuilder yaml = new StringBuilder();
processNode(root, yaml, 0);
return yaml.toString(); }
}
5.2. 处理不同节点类型
处理逻辑如下:
private void processNode(JsonNode jsonNode, StringBuilder yaml, int depth) {
if (jsonNode.isValueNode()) {
yaml.append(jsonNode.asText());
}
else if (jsonNode.isArray()) {
for (JsonNode arrayItem : jsonNode) {
appendNodeToYaml(arrayItem, yaml, depth, true);
}
}
else if (jsonNode.isObject()) {
appendNodeToYaml(jsonNode, yaml, depth, false);
}
}
- Value 节点:直接使用
asText()
获取值。 - Array 节点:遍历数组,传入
appendNodeToYaml
。 - Object 节点:使用
fields()
获取字段名和值。
private void appendNodeToYaml(
JsonNode node, StringBuilder yaml, int depth, boolean isArrayItem) {
Iterator<Entry<String, JsonNode>> fields = node.fields();
boolean isFirst = true;
while (fields.hasNext()) {
Entry<String, JsonNode> jsonField = fields.next();
addFieldNameToYaml(yaml, jsonField.getKey(), depth, isArrayItem && isFirst);
processNode(jsonField.getValue(), yaml, depth+1);
isFirst = false;
}
}
private void addFieldNameToYaml(
StringBuilder yaml, String fieldName, int depth, boolean isFirstInArray) {
if (yaml.length()>0) {
yaml.append("\n");
int requiredDepth = (isFirstInArray) ? depth-1 : depth;
for(int i = 0; i < requiredDepth; i++) {
yaml.append(" ");
}
if (isFirstInArray) {
yaml.append("- ");
}
}
yaml.append(fieldName);
yaml.append(": ");
}
6. 总结
本文涵盖了 Jackson 树模型中的常用 API 和操作场景。
所有示例代码均可在 GitHub 仓库中找到:**GitHub 项目地址**。