1. 概述
本教程將重點介紹使用 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 來源解析
此方法在 Jackson – Marshall String to JsonNode 文章中得到了充分的闡述。請參閲該文章以獲取更多信息。
2.3. 從對象轉換
一個節點可以通過調用 ObjectMapper 對象的 <em>valueToTree(Object fromValue)</em> 方法,從 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. 轉換一個 Node
/**
* This function transforms a Node object into a new Node object.
* It modifies the original Node object.
*
* @param {Node} node The Node object to transform.
* @returns {Node} The transformed Node object.
*/
function transformNode(node) {
// Perform the transformation logic here.
// This could involve updating properties,
// adding new attributes, or removing existing ones.
// Example:
// node.name = "New Name";
// node.value = node.value * 2;
return node;
}
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. 將 JSON 節點轉換為對象
最方便的方法是將 JsonNode 對象轉換為 Java 對象,就是使用 treeToValue API:
NodeBean toValue = mapper.treeToValue(node, NodeBean.class);這與以下內容功能上等效:
NodeBean toValue = mapper.convertValue(node, NodeBean.class)我們也可以通過令牌流來實現:
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 元素,存儲在名為 example.json 的文件中,作為執行操作的基礎結構:
{
"name":
{
"first": "Tatu",
"last": "Saloranta"
},
"title": "Jackson founder",
"company": "FasterXML"
}<p>此JSON文件位於類路徑上,被解析為一個模型樹。</p>
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. 修改節點
可以通過調用 set(String fieldName, JsonNode value) 方法來修改一個 ObjectNode 實例:
JsonNode locatedNode = locatedNode.set(fieldName, value);類似的結果可以通過在相同類型的對象上使用 replace 或 setAll 方法來實現。
為了驗證該方法是否按預期工作,我們將從 first 和 last 對象更改根節點下的 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) 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。
該測試將向我們的 toYaml 方法提供 JSON 文檔的根節點,並斷言返回的值是我們期望的值:
@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;
}
}我們無法通過節點來確定其祖先節點數量。
因此,我們向 processNode 方法傳遞一個名為 depth 的字段,用於跟蹤此信息,並在每次獲取子節點時增加該值,以便正確地縮進我們的 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 以及相關的場景。