1. 概述
XML 的一大优势在于其强大的处理能力,特别是 XPath,它由 W3C 标准 定义。而针对 JSON,社区也诞生了类似的工具 —— JsonPath。
本篇文章将带你入门 Jayway JsonPath,这是 JSONPath 规范 的一个 Java 实现。我们将介绍其环境搭建、语法、常用 API 以及典型使用场景。
2. 环境搭建
要在项目中使用 JsonPath,只需在 Maven 的 pom.xml
中添加以下依赖即可:
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.9.0</version>
</dependency>
3. 语法
我们以如下 JSON 结构为例,来讲解 JsonPath 的语法和常用 API:
{
"tool": {
"jsonpath": {
"creator": {
"name": "Jayway Inc.",
"location": [
"Malmo",
"San Francisco",
"Helsingborg"
]
}
}
},
"book": [
{
"title": "Beginning JSON",
"price": 49.99
},
{
"title": "JSON at Work",
"price": 29.99
}
]
}
3.1. 表示法
JsonPath 使用特殊的符号来表示节点及其连接关系。主要有两种表示方式:点号(dot)和方括号(bracket)。
下面两个路径都指向同一个节点 —— 也就是 tool
下的 jsonpath.creator.location
数组中的第三个元素:
点号表示法:
$.tool.jsonpath.creator.location[2]
方括号表示法:
$['tool']['jsonpath']['creator']['location'][2]
其中,$
表示 JSON 的根节点。
3.2. 操作符
JsonPath 提供了一些实用的操作符:
✅ 根节点($):表示 JSON 结构的根对象或数组,前面已展示用法。
✅ 当前节点(@):表示当前正在处理的节点,常用于过滤器中。例如,表达式 book[?(@.price == 49.99)]
会筛选出价格为 49.99 的书籍。
✅ 通配符(*):匹配指定范围内的所有元素。例如 book[*]
表示 book
数组中的所有元素。
3.3. 函数与过滤器
JsonPath 还支持在路径末尾使用函数,用于合成路径结果,例如:
min()
:最小值max()
:最大值avg()
:平均值stddev()
:标准差length()
:元素个数
此外,过滤器(Filter)是布尔表达式,用于限制返回结果,只保留满足条件的节点。
常见的过滤条件包括:
- 相等:
==
- 正则匹配:
=~
- 包含:
in
- 是否为空:
empty
更多操作符、函数和过滤器的详细说明,可参考 JsonPath GitHub 项目。
4. 常见操作
本节示例均基于前面定义的 JSON 示例结构。
4.1. 访问 JSON 文档
JsonPath 提供了静态 read
方法来访问 JSON 文档:
<T> T JsonPath.read(String jsonString, String jsonPath, Predicate... filters);
也可以使用链式调用:
<T> T JsonPath.parse(String jsonString).read(String jsonPath, Predicate... filters);
此外,read
方法支持多种数据源类型,包括 Object
、InputStream
、URL
和 File
。
为了简化演示,我们先定义两个路径:
String jsonpathCreatorNamePath = "$['tool']['jsonpath']['creator']['name']";
String jsonpathCreatorLocationPath = "$['tool']['jsonpath']['creator']['location'][*]";
然后解析 JSON 并读取数据:
DocumentContext jsonContext = JsonPath.parse(jsonDataSourceString);
String jsonpathCreatorName = jsonContext.read(jsonpathCreatorNamePath);
List<String> jsonpathCreatorLocation = jsonContext.read(jsonpathCreatorLocationPath);
第一个调用返回的是字符串(创建者名称),第二个返回的是地址列表。
使用 JUnit 进行断言验证:
assertEquals("Jayway Inc.", jsonpathCreatorName);
assertThat(jsonpathCreatorLocation.toString(), containsString("Malmo"));
assertThat(jsonpathCreatorLocation.toString(), containsString("San Francisco"));
assertThat(jsonpathCreatorLocation.toString(), containsString("Helsingborg"));
4.2. 使用过滤器(Predicates)
我们扩展一下 JSON 示例,用于演示过滤器的使用:
{
"book": [
{
"title": "Beginning JSON",
"author": "Ben Smith",
"price": 49.99
},
{
"title": "JSON at Work",
"author": "Tom Marrs",
"price": 29.99
},
{
"title": "Learn JSON in a DAY",
"author": "Acodemy",
"price": 8.99
},
{
"title": "JSON: Questions and Answers",
"author": "George Duckett",
"price": 6.00
}
],
"price range": {
"cheap": 10.00,
"medium": 20.00
}
}
过滤器用于筛选出满足条件的对象。我们可以使用 Filter
类来创建过滤器:
Filter expensiveFilter = Filter.filter(Criteria.where("price").gt(20.00));
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?]", expensiveFilter);
也可以自定义 Predicate
:
Predicate expensivePredicate = new Predicate() {
public boolean apply(PredicateContext context) {
String value = context.item(Map.class).get("price").toString();
return Float.valueOf(value) > 20.00;
}
};
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?]", expensivePredicate);
或者使用内联方式(inline predicate):
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?(@['price'] > $['price range']['medium'])]");
以上三种方式的验证结果如下:
private void predicateUsageAssertionHelper(List<?> predicate) {
assertThat(predicate.toString(), containsString("Beginning JSON"));
assertThat(predicate.toString(), containsString("JSON at Work"));
assertThat(predicate.toString(), not(containsString("Learn JSON in a DAY")));
assertThat(predicate.toString(), not(containsString("JSON: Questions and Answers")));
}
5. 配置
5.1. 选项(Options)
Jayway JsonPath 提供了多个选项来定制其行为:
- ✅
Option.AS_PATH_LIST
:返回路径列表,而非值 - ✅
Option.DEFAULT_PATH_LEAF_TO_NULL
:缺失叶子节点时返回 null - ✅
Option.ALWAYS_RETURN_LIST
:即使路径唯一也返回列表 - ✅
Option.SUPPRESS_EXCEPTIONS
:不抛出异常 - ✅
Option.REQUIRE_PROPERTIES
:路径中缺失属性时报错
创建新配置:
Configuration configuration = Configuration.builder().options(Option.<OPTION>).build();
添加到已有配置:
Configuration newConfiguration = configuration.addOptions(Option.<OPTION>);
5.2. SPI 接口
对于更复杂的使用场景,可以通过 SPI 接口来自定义 JsonPath 的行为:
- ✅
JsonProvider
:控制 JSON 的解析和处理方式 - ✅
MappingProvider
:控制节点值与返回对象类型的绑定 - ✅
CacheProvider
:控制路径缓存机制,提升性能
6. 使用示例
假设我们有一个电影信息服务,返回如下结构的 JSON 数据:
[
{
"id": 1,
"title": "Casino Royale",
"director": "Martin Campbell",
"starring": ["Daniel Craig", "Eva Green"],
"desc": "Twenty-first James Bond movie",
"release date": 1163466000000,
"box office": 594275385
},
{
"id": 2,
"title": "Quantum of Solace",
"director": "Marc Forster",
"starring": ["Daniel Craig", "Olga Kurylenko"],
"desc": "Twenty-second James Bond movie",
"release date": 1225242000000,
"box office": 591692078
},
{
"id": 3,
"title": "Skyfall",
"director": "Sam Mendes",
"starring": ["Daniel Craig", "Naomie Harris"],
"desc": "Twenty-third James Bond movie",
"release date": 1350954000000,
"box office": 1110526981
},
{
"id": 4,
"title": "Spectre",
"director": "Sam Mendes",
"starring": ["Daniel Craig", "Lea Seydoux"],
"desc": "Twenty-fourth James Bond movie",
"release date": 1445821200000,
"box office": 879376275
}
]
其中 release date
是毫秒时间戳,box office
表示票房收入。
6.1. 根据 ID 获取电影信息
Object dataObject = JsonPath.parse(jsonString).read("$[?(@.id == 2)]");
String dataString = dataObject.toString();
assertThat(dataString, containsString("2"));
assertThat(dataString, containsString("Quantum of Solace"));
assertThat(dataString, containsString("Twenty-second James Bond movie"));
6.2. 根据主演查找电影名称
@Test
public void givenStarring_whenRequestingMovieTitle_thenSucceed() {
List<Map<String, Object>> dataList = JsonPath.parse(jsonString)
.read("$[?('Eva Green' in @['starring'])]");
String title = (String) dataList.get(0).get("title");
assertEquals("Casino Royale", title);
}
6.3. 计算总票房
@Test
public void givenCompleteStructure_whenCalculatingTotalRevenue_thenSucceed() {
DocumentContext context = JsonPath.parse(jsonString);
int length = context.read("$.length()");
long revenue = 0;
for (int i = 0; i < length; i++) {
revenue += context.read("$[" + i + "]['box office']", Long.class);
}
assertEquals(594275385L + 591692078L + 1110526981L + 879376275L, revenue);
}
6.4. 票房最高的电影
DocumentContext context = JsonPath.parse(jsonString);
List<Object> revenueList = context.read("$[*]['box office']");
Integer[] revenueArray = revenueList.toArray(new Integer[0]);
Arrays.sort(revenueArray);
int highestRevenue = revenueArray[revenueArray.length - 1];
Configuration pathConfiguration =
Configuration.builder().options(Option.AS_PATH_LIST).build();
List<String> pathList = JsonPath.using(pathConfiguration).parse(jsonString)
.read("$[?(@['box office'] == " + highestRevenue + ")]");
Map<String, String> dataRecord = context.read(pathList.get(0));
String title = dataRecord.get("title");
assertEquals("Skyfall", title);
6.5. 导演最新作品
DocumentContext context = JsonPath.parse(jsonString);
List<Map<String, Object>> samMendesMovies = context.read("$[?(@.director == 'Sam Mendes')]");
List<Object> dateList = new ArrayList<>();
for (Map<String, Object> item : samMendesMovies) {
Object date = item.get("release date");
dateList.add(date);
}
Long[] dateArray = dateList.toArray(new Long[0]);
Arrays.sort(dateArray);
long latestTime = dateArray[dateArray.length - 1];
List<Map<String, Object>> finalDataList = context.read("$[?(@['director']
== 'Sam Mendes' && @['release date'] == " + latestTime + ")]");
String title = (String) finalDataList.get(0).get("title");
assertEquals("Spectre", title);
7. 总结
本文介绍了 Jayway JsonPath 的核心功能,它是一个强大的 JSON 解析与遍历工具。
虽然 JsonPath 在处理父节点、兄弟节点等方面存在局限,但在大多数场景下依然非常实用。
所有示例代码可在 GitHub 查看。