1. Overview
本文将重点介绍如何在Jackson中使用树模型节点。
我们将使用JsonNode进行各种转换以及添加、修改和删除节点。
2. 创建一个节点
创建节点的第一步是使用默认构造函数实例化一个ObjectMapper对象:
ObjectMapper mapper = new ObjectMapper();
由于创建ObjectMapper对象的开销很大,因此建议在多个操作中重用同一个对象。
接下来,在创建了ObjectMapper之后,我们有三种不同的方法来创建树节点。
2.1. 从头开始构造一个节点
这是从无到有创建节点最常见的方法:
JsonNode node = mapper.createObjectNode();
或者,我们也可以通过JsonNodeFactory创建一个节点:
JsonNode node = JsonNodeFactory.instance.objectNode();
2.2. 从JSON源进行解析
如果出于某种原因,你需要更低层次的解析,下面的例子暴露了负责实际解析字符串的JsonParser
:
@Test
public void givenUsingLowLevelApi_whenParsingJsonStringIntoJsonNode_thenCorrect()
throws JsonParseException, IOException {
String jsonString = "{"k1":"v1","k2":"v2"}";
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();
JsonParser parser = factory.createParser(jsonString);
JsonNode actualObj = mapper.readTree(parser);
assertNotNull(actualObj);
}
2.3. 从对象转换
节点可以通过调用ObjectMapper上的*valueToTree(Object fromValue)*方法从Java对象转换过来:
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;
}
// standard getters and setters
}
让我们编写一个测试来确保转换正确发生:
@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. 转换为对象
将JsonNode转换为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元素,它们包含在一个名为exampl.Jsone的文件中,作为要执行的操作的基本结构:
{
"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. 定位一个节点
在处理任何节点之前,我们需要做的第一件事是定位它并将其分配给一个变量。
如果我们事先知道到节点的路径,这很容易做到。
假设我们想要一个名为last的节点,它位于name节点下:
JsonNode locatedNode = rootNode.path("name").path("last");
或者,也可以使用 get 或 with api来代替 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. 编辑一个节点
一个ObjectNode实例可以通过调用 set(String fieldName, JsonNode value) 方法来修改:
JsonNode locatedNode = locatedNode.set(fieldName, value);
在相同类型的对象上使用 replace 或 setAll 方法也可以获得类似的结果。
为了验证该方法是否如预期的那样工作,我们将根节点下的字段name的值从一个first和last对象更改为另一个只包含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) API来删除:
JsonNode removedNode = locatedNode.remove(fieldName);
为了一次删除多个节点,我们可以调用一个带有 Collection<String>
类型参数的重载方法,该方法返回父节点而不是要删除的节点:
ObjectNode locatedNode = locatedNode.remove(fieldNames);
在极端情况下,当我们想要删除给定节点的所有子节点时,removeAll API就派上用场了。
下面的测试将关注上面提到的第一种方法,这是最常见的场景:
@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有三种类型的节点,分别是值、对象和数组。
因此,让我们通过添加一个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
我们知道JSON节点具有层次树结构。因此,遍历整个JSON文档最简单的方法是从顶部开始,向下遍历所有子节点。
我们将把根节点传递给递归方法。然后,该方法将使用所提供节点的每个子节点调用自己。
5.1. 测试迭代
首先,我们将创建一个简单的测试,检查是否可以成功地将JSON转换为YAML。
我们的测试将JSON文档的根节点提供给我们的toYaml方法,并断言返回值是我们所期望的:
@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. 处理不同节点类型
我们需要以略微不同的方式处理不同类型的节点。
我们将在processNode方法中实现这一点:
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方法来获得值的String表示形式。
接下来,让我们看看Array节点。Array节点中的每个项本身都是JsonNode,因此我们遍历Array并将每个节点传递给appendNodeToYaml方法。我们还需要知道这些节点是数组的一部分。
不幸的是,节点本身不包含任何告诉我们这一点的内容,因此我们将向appendNodeToYaml方法传递一个标志。
最后,我们希望遍历每个Object节点的所有子节点。一种选择是使用 JsonNode.elements。
然而,我们无法从元素中确定字段名,因为它只包含字段值:
Object {"first": "Tatu", "last": "Saloranta"}
Value "Jackson Founder"
Value "FasterXML"
Array [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]
相反,我们将使用 JsonNode.fields,因为这让我们可以访问字段名和值:
Key="name", Value=Object {"first": "Tatu", "last": "Saloranta"}
Key="title", Value=Value "Jackson Founder"
Key="company", Value=Value "FasterXML"
Key="pets", Value=Array [{"type": "dog", "number": 1},{"type": "fish", "number": 50}]
对于每个字段,我们将字段名添加到输出中,然后将值作为子节点处理,将其传递给processNode方法:
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;
}
}
我们无法从节点上判断它有多少祖先。
因此,我们将一个名为depth的字段传递给processNode方法来跟踪这个值,并且在每次获得子节点时增加这个值,以便我们能够正确地缩进YAML输出中的字段:
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(": ");
}
现在,我们已经有了遍历节点并创建YAML输出的所有代码,我们可以运行测试来显示它是否工作。
6. 结尾
本文介绍了在Jackson中使用树模型时的常用api和场景。