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 方法支持多种数据源类型,包括 ObjectInputStreamURLFile

为了简化演示,我们先定义两个路径:

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 查看。


原始标题:Introduction to JsonPath