• 在Jackson中使用树模型节点(JsonNode)详解


    1. Overview

    本文将重点介绍如何在Jackson中使用树模型节点。

    我们将使用JsonNode进行各种转换以及添加、修改和删除节点。

    2. 创建一个节点

    创建节点的第一步是使用默认构造函数实例化一个ObjectMapper对象:

    ObjectMapper mapper = new ObjectMapper();
    
    • 1

    由于创建ObjectMapper对象的开销很大,因此建议在多个操作中重用同一个对象。

    接下来,在创建了ObjectMapper之后,我们有三种不同的方法来创建树节点。

    2.1. 从头开始构造一个节点

    这是从无到有创建节点最常见的方法:

    JsonNode node = mapper.createObjectNode();
    
    • 1

    或者,我们也可以通过JsonNodeFactory创建一个节点:

    JsonNode node = JsonNodeFactory.instance.objectNode();
    
    • 1

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.3. 从对象转换

    节点可以通过调用ObjectMapper上的*valueToTree(Object fromValue)*方法从Java对象转换过来:

    JsonNode node = mapper.valueToTree(fromValue);
    
    • 1

    convertValue API在这里也很有用:

    JsonNode node = mapper.convertValue(fromValue, JsonNode.class);
    
    • 1

    让我们看看它是如何工作的。

    假设我们有一个名为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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    让我们编写一个测试来确保转换正确发生:

    @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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3. 转换节点

    3.1. 以JSON格式输出

    这是将树节点转换为JSON字符串的基本方法,其中目标可以是 FileOutputStreamWriter:

    mapper.writeValue(destination, node);
    
    • 1

    通过重用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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    3.2. 转换为对象

    JsonNode转换为Java对象最方便的方法是treeToValue API:

    NodeBean toValue = mapper.treeToValue(node, NodeBean.class);
    
    • 1

    这在功能上等价于以下内容:

    NodeBean toValue = mapper.convertValue(node, NodeBean.class)
    
    • 1

    我们也可以通过token流来做到这一点:

    JsonParser parser = mapper.treeAsTokens(node);
    NodeBean toValue = mapper.readValue(parser, NodeBean.class);
    
    • 1
    • 2

    最后,让我们实现一个验证转换过程的测试:

    @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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4. 操纵树节点

    我们将使用以下JSON元素,它们包含在一个名为exampl.Jsone的文件中,作为要执行的操作的基本结构:

    {
        "name": 
            {
                "first": "Tatu",
                "last": "Saloranta"
            },
    
        "title": "Jackson founder",
        "company": "FasterXML"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    注意,在说明对以下小节中的节点的操作时,将使用树的根。

    4.1. 定位一个节点

    在处理任何节点之前,我们需要做的第一件事是定位它并将其分配给一个变量。

    如果我们事先知道到节点的路径,这很容易做到。

    假设我们想要一个名为last的节点,它位于name节点下:

    JsonNode locatedNode = rootNode.path("name").path("last");
    
    • 1

    或者,也可以使用 getwith api来代替 path

    如果路径未知,搜索当然会变得更加复杂和迭代。

    我们可以在第5节-遍历节点中看到遍历所有节点的例子。

    4.2. 添加新节点

    一个节点可以被添加为另一个节点的子节点:

    ObjectNode newNode = ((ObjectNode) locatedNode).put(fieldName, value);
    
    • 1

    put的许多重载变体可用于添加不同值类型的新节点。

    还有许多其他类似的方法可用,包括putArrayputObjectPutPOJOputRawValueputNull

    最后,让我们看一个例子,我们添加了一个完整的结构到树的根节点:

    "address":
    {
        "city": "Seattle",
        "state": "Washington",
        "country": "United States"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    以下是经过所有这些操作并验证结果的完整测试:

    @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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    4.3. 编辑一个节点

    一个ObjectNode实例可以通过调用 set(String fieldName, JsonNode value) 方法来修改:

    JsonNode locatedNode = locatedNode.set(fieldName, value);
    
    • 1

    在相同类型的对象上使用 replacesetAll 方法也可以获得类似的结果。

    为了验证该方法是否如预期的那样工作,我们将根节点下的字段name的值从一个firstlast对象更改为另一个只包含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());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4.4. 删除一个节点

    一个节点可以通过调用其父节点上的 remove(String fieldName) API来删除:

    JsonNode removedNode = locatedNode.remove(fieldName);
    
    • 1

    为了一次删除多个节点,我们可以调用一个带有 Collection 类型参数的重载方法,该方法返回父节点而不是要删除的节点:

    ObjectNode locatedNode = locatedNode.remove(fieldNames);
    
    • 1

    在极端情况下,当我们想要删除给定节点的所有子节点时,removeAll API就派上用场了。

    下面的测试将关注上面提到的第一种方法,这是最常见的场景:

    @Test
    public void givenANode_whenRemovingFromATree_thenCorrect() throws IOException {
        JsonNode rootNode = ExampleStructure.getExampleRoot();
        ((ObjectNode) rootNode).remove("company");
    
        assertTrue(rootNode.path("company").isMissingNode());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    5. 遍历节点

    让我们遍历JSON文档中的所有节点并将它们重新格式化为YAML。

    JSON有三种类型的节点,分别是值、对象和数组。

    因此,让我们通过添加一个Array来确保我们的示例数据具有所有三种不同的类型:

    {
        "name": 
            {
                "first": "Tatu",
                "last": "Saloranta"
            },
    
        "title": "Jackson founder",
        "company": "FasterXML",
        "pets" : [
            {
                "type": "dog",
                "number": 1
            },
            {
                "type": "fish",
                "number": 50
            }
        ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    现在让我们看看我们想要生成的YAML:

    name: 
      first: Tatu
      last: Saloranta
    title: Jackson founder
    company: FasterXML
    pets: 
    - type: dog
      number: 1
    - type: fish
      number: 50
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们知道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(); }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    首先,让我们考虑一个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}]
    
    • 1
    • 2
    • 3
    • 4

    相反,我们将使用 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}]
    
    • 1
    • 2
    • 3
    • 4

    对于每个字段,我们将字段名添加到输出中,然后将值作为子节点处理,将其传递给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;
        }
            
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们无法从节点上判断它有多少祖先。

    因此,我们将一个名为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(": ");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    现在,我们已经有了遍历节点并创建YAML输出的所有代码,我们可以运行测试来显示它是否工作。

    6. 结尾

    本文介绍了在Jackson中使用树模型时的常用api和场景。

  • 相关阅读:
    vue2(4)
    积水监测系统——方便、快速、及时、多测点同时监测
    [C语言]排序的大乱炖——喵喵的成长记
    深度学习之情感分析
    《BEV LaneDet:Fast Lane Detection on BEV Ground》论文笔记
    RDD行动算子和血缘关系
    【汇编语言】3.汇编语言程序
    编译安装apache
    外贸出口迷你车载冰箱亚马逊UL2089测试标准
    客户端发现pod并与之通信
  • 原文地址:https://blog.csdn.net/wjw465150/article/details/127561063