
本教程详细探讨了在spring boot应用中如何高效地从复杂嵌套json结构中提取特定数据。我们将重点介绍jackson库的两种核心方法:jackson streaming api,适用于处理大型或结构动态的json,以及jackson data binding,适用于将JSon映射到预定义java对象。文章将提供详细的代码示例和实现步骤,并讨论如何进一步过滤提取出的数据,例如按类别筛选。
在spring boot应用程序中处理来自api调用的json数据是常见的任务,尤其当JSON结构包含多层嵌套时,如何高效且优雅地提取所需信息成为一个关键问题。本文将深入探讨使用Jackson库来解决这一挑战的两种主要方法:Jackson streaming API和Jackson Data Binding,并提供具体的代码示例。
1. 准备工作
在Spring Boot项目中,Jackson是默认的json处理库,因此通常无需额外添加依赖。但如果需要确保版本或解决特定问题,可以检查 pom.xml 中是否包含以下依赖:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
假设我们有以下JSON数据,其中包含嵌套的 footwearList 和 clothingList:
[ { "id": 1, "footwearList": [ { "id": 1, "name": "sandals", "category": "men" }, { "id": 3, "name": "sandals", "category": "women" } ], "clothingList": [ { "id": 1, "name": "t-shirt", "category": "men" }, { "id": 3, "name": "tshirt", "category": "women" } ] }, { "id": 2, "footwearList": [ { "id": 2, "name": "shoes", "category": "men" }, { "id": 4, "name": "shoes", "category": "women" } ], "clothingList": [ { "id": 2, "name": "shirt", "category": "men" }, { "id": 4, "name": "shirt", "category": "women" } ] } ]
我们的目标是从这个JSON中提取 footwearList 和 clothingList 中的 Item 对象,并可能进一步按 category 进行筛选。
2. 使用Jackson Streaming API
当JSON结构非常庞大、键名动态不固定,或者我们不希望将整个JSON完全映射为Java对象时,Jackson Streaming API提供了一种高效的迭代方式。它允许我们遍历JSON树,按需提取数据。
首先,定义一个 Item 类来表示 footwearList 和 clothingList 中的元素:
public class Item { private int id; private String name; private String category; // 无参构造函数 public Item() {} // 带参构造函数 public Item(int id, String name, String category) { this.id = id; this.name = name; this.category = category; } // Getters and Setters public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } @Override public String toString() { return "Item{id=" + id + ", name='" + name + "', category='" + category + "'}"; } }
接下来,我们创建一个辅助方法将 Jsonnode 转换为 Item 对象:
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; public class JsonExtractor { private static final Objectmapper MAPPER = new ObjectMapper(); // 辅助方法:将JsonNode转换为Item对象 public static Item nodeToItem(JsonNode node) { try { return MAPPER.treeToValue(node, Item.class); } catch (com.fasterxml.jackson.core.JsonProcessingException e) { System.err.println("Error converting JsonNode to Item: " + e.getMessage()); return null; } } // ... 其他方法 }
现在,我们可以编写逻辑来遍历JSON并提取嵌套列表中的 Item:
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import java.util.regex.Pattern; import java.util.function.Predicate; import java.util.stream.StreamSupport; import java.util.Collections; // 导入Collections public class JsonExtractor { private static final ObjectMapper MAPPER = new ObjectMapper(); // 定义一个正则表达式谓词,用于匹配包含"footwear"或"clothing"的字段名 public static final Predicate<String> TARGET_LIST_PREDICATE = Pattern.compile("footwear|clothing").asPredicate(); // 辅助方法:将JsonNode转换为Item对象 (同上) public static Item nodeToItem(JsonNode node) { try { return MAPPER.treeToValue(node, Item.class); } catch (com.fasterxml.jackson.core.JsonProcessingException e) { System.err.println("Error converting JsonNode to Item: " + e.getMessage()); return null; } } public List<Item> extractNestedItems(String json) { try { JsonNode rootNode = MAPPER.readTree(json); if (!rootNode.isArray()) { System.err.println("Root JSON node is not an array."); return Collections.emptyList(); } return StreamSupport.stream(rootNode.spliterator(), false) // 遍历根数组的每个对象 .<JsonNode>mapMulti((node, consumer) -> { // 对于每个对象 node.fields().forEachRemaining(entry -> { // 遍历其所有字段 if (TARGET_LIST_PREDICATE.test(entry.getKey()) && entry.getValue().isArray()) { // 如果字段名匹配且其值为数组,则将数组中的每个元素传递给consumer entry.getValue().forEach(consumer); } }); }) .map(JsonExtractor::nodeToItem) // 将每个JsonNode转换为Item对象 .toList(); // 收集结果 } catch (com.fasterxml.jackson.core.JsonProcessingException e) { System.err.println("Error parsing JSON: " + e.getMessage()); return Collections.emptyList(); } } // 示例用法 public static void main(String[] args) { String json = "[n" + " {n" + " "id": 1,n" + " "footwearList": [n" + " { "id": 1, "name": "sandals", "category": "men" },n" + " { "id": 3, "name": "sandals", "category": "women" }n" + " ],n" + " "clothingList": [n" + " { "id": 1, "name": "t-shirt", "category": "men" },n" + " { "id": 3, "name": "tshirt", "category": "women" }n" + " ]n" + " },n" + " {n" + " "id": 2,n" + " "footwearList": [n" + " { "id": 2, "name": "shoes", "category": "men" },n" + " { "id": 4, "name": "shoes", "category": "women" }n" + " ],n" + " "clothingList": [n" + " { "id": 2, "name": "shirt", "category": "men" },n" + " { "id": 4, "name": "shirt", "category": "women" }n" + " ]n" + " }n" + "]"; JsonExtractor extractor = new JsonExtractor(); List<Item> allItems = extractor.extractNestedItems(json); System.out.println("所有提取的Item:"); allItems.forEach(System.out::println); // 进一步按类别过滤 List<Item> menItems = allItems.stream() .filter(item -> "men".equals(item.getCategory())) .toList(); System.out.println("n按类别'men'过滤的Item:"); menItems.forEach(System.out::println); } }
输出示例:
Easily find JSON paths within JSON objects using our intuitive Json Path Finder
30 所有提取的Item: Item{id=1, name='sandals', category='men'} Item{id=3, name='sandals', category='women'} Item{id=1, name='t-shirt', category='men'} Item{id=3, name='tshirt', category='women'} Item{id=2, name='shoes', category='men'} Item{id=4, name='shoes', category='women'} Item{id=2, name='shirt', category='men'} Item{id=4, name='shirt', category='women'} 按类别'men'过滤的Item: Item{id=1, name='sandals', category='men'} Item{id=1, name='t-shirt', category='men'} Item{id=2, name='shoes', category='men'} Item{id=2, name='shirt', category='men'}
这种方法非常灵活,即使JSON的顶层结构或嵌套列表的键名发生变化,只要我们调整 TARGET_LIST_PREDICATE,代码仍然可以工作。
3. 使用Jackson Data Binding
对于结构相对固定且已知其模式的JSON数据,Jackson Data Binding是更推荐和简洁的方法。它允许我们将JSON直接映射到Java对象(POJO),极大地简化了数据访问。
首先,定义一个 Store 类来表示JSON中的顶层对象。由于 footwearList 和 clothingList 的键是动态的,我们可以使用 Map<String, List<Item>> 结合 @JsonAnySetter 和 @JsonAnyGetter 来处理。
import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import java.util.HashMap; import java.util.List; import java.util.Map; public class Store { private int id; private Map<String, List<Item>> items = new HashMap<>(); // 用于存储动态的列表 // 无参构造函数 public Store() {} // Getters and Setters for id public int getId() { return id; } public void setId(int id) { this.id = id; } // @JsonAnySetter 用于反序列化时捕获未映射的键值对 @JsonAnySetter public void readStore(String key, List<Item> value) { this.items.put(key, value); } // @JsonAnyGetter 用于序列化时将Map中的内容作为顶级字段输出 // 注意:这里的实现是为了演示,实际序列化时可能需要更精细的控制 @JsonAnyGetter public Map<String, List<Item>> getItems() { return items; } @Override public String toString() { return "Store{id=" + id + ", items=" + items + '}'; } }
现在,我们可以使用 ObjectMapper 将整个JSON字符串反序列化为 List<Store>:
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; public class JsonDataBinder { private static final ObjectMapper MAPPER = new ObjectMapper(); public List<Store> parseJsonToStores(String json) { try { return MAPPER.readValue(json, new TypeReference<List<Store>>() {}); } catch (com.fasterxml.jackson.core.JsonProcessingException e) { System.err.println("Error parsing JSON to Store list: " + e.getMessage()); return Collections.emptyList(); } } public static void main(String[] args) { String json = "[n" + " {n" + " "id": 1,n" + " "footwearList": [n" + " { "id": 1, "name": "sandals", "category": "men" },n" + " { "id": 3, "name": "sandals", "category": "women" }n" + " ],n" + " "clothingList": [n" + " { "id": 1, "name": "t-shirt", "category": "men" },n" + " { "id": 3, "name": "tshirt", "category": "women" }n" + " ]n" + " },n" + " {n" + " "id": 2,n" + " "footwearList": [n" + " { "id": 2, "name": "shoes", "category": "men" },n" + " { "id": 4, "name": "shoes", "category": "women" }n" + " ],n" + " "clothingList": [n" + " { "id": 2, "name": "shirt", "category": "men" },n" + " { "id": 4, "name": "shirt", "category": "women" }n" + " ]n" + " }n" + "]"; JsonDataBinder binder = new JsonDataBinder(); List<Store> stores = binder.parseJsonToStores(json); System.out.println("所有解析的Store对象:"); stores.forEach(System.out::println); // 提取所有鞋类商品 List<Item> allFootwear = stores.stream() .flatMap(store -> store.getItems().getOrDefault("footwearList", Collections.emptyList()).stream()) .collect(Collectors.toList()); System.out.println("n所有鞋类商品:"); allFootwear.forEach(System.out::println); // 提取所有男士服装 List<Item> menClothing = stores.stream() .flatMap(store -> store.getItems().getOrDefault("clothingList", Collections.emptyList()).stream()) .filter(item -> "men".equals(item.getCategory())) .collect(Collectors.toList()); System.out.println("n所有男士服装:"); menClothing.forEach(System.out::println); } }
输出示例:
所有解析的Store对象: Store{id=1, items={footwearList=[Item{id=1, name='sandals', category='men'}, Item{id=3, name='sandals', category='women'}], clothingList=[Item{id=1, name='t-shirt', category='men'}, Item{id=3, name='tshirt', category='women'}]}} Store{id=2, items={footwearList=[Item{id=2, name='shoes', category='men'}, Item{id=4, name='shoes', category='women'}], clothingList=[Item{id=2, name='shirt', category='men'}, Item{id=4, name='shirt', category='women'}]}} 所有鞋类商品: Item{id=1, name='sandals', category='men'} Item{id=3, name='sandals', category='women'} Item{id=2, name='shoes', category='men'} Item{id=4, name='shoes', category='women'} 所有男士服装: Item{id=1, name='t-shirt', category='men'} Item{id=2, name='shirt', category='men'}
Data Binding方法将整个JSON结构映射为强类型Java对象,使得后续的数据访问和操作(如使用Java Stream API进行过滤)变得非常直观和类型安全。
4. 注意事项与总结
- 选择合适的解析策略:
- Jackson Streaming API: 适用于JSON结构不固定、键名动态、数据量非常大(避免一次性加载所有数据到内存)的场景。它提供了更细粒度的控制,但代码相对复杂。
- Jackson Data Binding: 适用于JSON结构相对稳定且可以预先定义POJO的场景。它提供了更高的开发效率和代码可读性,是大多数Spring Boot应用的首选。
- 错误处理: 在实际应用中,始终要包含 try-catch 块来处理 JsonProcessingException(或其子类 IOException),以优雅地处理JSON解析过程中可能出现的错误。
- 性能: 对于中小型JSON,Data Binding的性能通常足够好。对于超大型JSON,Streaming API可能提供更好的内存效率。
- JSONPath: 虽然原问题中提到了 JsonPath,它是一个强大的JSON查询语言。但在Spring Boot的Jackson生态系统中,通常可以直接利用Jackson本身的API来实现大部分需求,而无需引入额外的 JsonPath 依赖,从而保持项目依赖的简洁性。
通过掌握Jackson Streaming API和Data Binding这两种方法,开发者可以根据具体需求,在Spring Boot中灵活高效地处理各种复杂嵌套的JSON数据。在大多数情况下,Data Binding结合 @JsonAnySetter 等注解,能够以最简洁的方式解决嵌套和动态键的问题。
