Contents

Jayway JsonPat 简介

1. 概述

XML 的优点之一是处理的可用性——包括 XPath——被定义为W3C 标准 。对于 JSON,已经出现了一个名为 JSONPath 的类似工具。

本教程将介绍 Jayway JsonPath ,它是JSONPath 规范 的 Java 实现。它描述了设置、语法、常用 API 和用例演示。

在本文中,我们探讨如何配置 Spring REST 机制以利用我们用 Kryo 说明的二进制数据格式。此外,我们展示了如何使用 Google 协议缓冲区支持多种数据格式。

2. 设置

要使用 JsonPath,我们只需要在 Maven pom 中包含一个依赖项:

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.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 使用特殊符号来表示节点及其与 JsonPath 路径中相邻节点的连接。有两种表示法:点和括号。

以下两个路径都引用了上述 JSON 文档中的同一个节点,它是creator节点location字段中的第三个元素,即根节点下属于tooljsonpath对象的子节点。

首先,我们将看到带有点符号的路径:

$.tool.jsonpath.creator.location[2]

现在让我们看一下括号符号:

$['tool']['jsonpath']['creator']['location'][2]

美元符号 ($) 代表根成员对象。

3.2. 运营商

我们在 JsonPath 中有几个有用的运算符:

  • **根节点 ($)**表示 JSON 结构的根成员,无论它是对象还是数组。我们在上一小节中包含了使用示例。
  • **当前节点 (@)**表示正在处理的节点。我们主要将其用作谓词输入表达式的一部分。假设我们正在处理上述 JSON 文档中的book数组;表达式*book[?(@.price == 49.99)]*指的是该数组中的第一本书。
  • 通配符 (*) 表示指定范围内的所有元素。例如,book[*]表示book数组中的所有节点。

3.3. 函数和过滤器

JsonPath 还具有我们可以在路径末尾使用的函数来合成该路径的输出表达式:min()max()avg()stddev() 和length()

最后,我们有过滤器。这些是布尔表达式,用于将返回的节点列表限制为仅调用方法需要的节点列表。

一些例子是相等(==)、正则表达式匹配(=~)、包含(in)和检查空性(empty)。我们主要对谓词使用过滤器。

有关不同运算符、函数和过滤器的完整列表和详细说明,请参阅JsonPath GitHub 项目。

4. 操作

在我们开始操作之前,快速附注:本节使用我们之前定义的 JSON 示例结构。

4.1. 访问文件

JsonPath 有一种方便的方式来访问 JSON 文档。我们通过静态read API 来做到这一点:

<T> T JsonPath.read(String jsonString, String jsonPath, Predicate... filters);

read API 可以与静态流式 API 一起使用,以提供更大的灵活性:

<T> T JsonPath.parse(String jsonString).read(String jsonPath, Predicate... filters);

我们可以 为不同类型的 JSON 源使用其他重载的read变体,包括ObjectInputStreamURL 和File

为简单起见,这部分的测试不包括参数列表中的谓词(空varargs)。但我们将在后面的小节中讨论*predicates *。

让我们首先定义两个要处理的示例路径:

String jsonpathCreatorNamePath = "$['tool']['jsonpath']['creator']['name']";
String jsonpathCreatorLocationPath = "$['tool']['jsonpath']['creator']['location'][*]";

接下来,我们将通过解析给定的 JSON 源jsonDataSourceString创建一个DocumentContext对象。然后,新创建的对象将用于使用上面定义的路径读取内容:

DocumentContext jsonContext = JsonPath.parse(jsonDataSourceString);
String jsonpathCreatorName = jsonContext.read(jsonpathCreatorNamePath);
List<String> jsonpathCreatorLocation = jsonContext.read(jsonpathCreatorLocationPath);

第一个read API 返回一个包含 JsonPath 创建者名称的string,而第二个返回其地址列表。

我们将使用 JUnit Assert API 来确认这些方法是否按预期工作:

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
    }
}

谓词确定过滤器的真或假输入值,以将返回的列表缩小到仅匹配的对象或数组。我们可以通过将Predicate用作静态工厂方法的参数,轻松地将Predicate集成 到Filter中。然后可以使用该Filter从 JSON 字符串中读取请求的内容:

Filter expensiveFilter = Filter.filter(Criteria.where("price").gt(20.00));
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
  .read("$['book'][?]", expensiveFilter);
predicateUsageAssertionHelper(expensive);

我们还可以定义我们自定义的Predicate并将其用作read API 的参数:

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);
predicateUsageAssertionHelper(expensive);

最后,一个谓词可以直接应用于read API 而无需创建任何对象,这称为内联谓词:

List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
  .read("$['book'][?(@['price'] > $['price range']['medium'])]");
predicateUsageAssertionHelper(expensive);

上面的所有三个Predicate示例都在以下断言辅助方法的帮助下进行了验证:

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.选项

Jayway JsonPath 提供了几个选项来调整默认配置:

  • Option.AS_PATH_LIST 返回评估命中的路径而不是它们的值。
  • Option.DEFAULT_PATH_LEAF_TO_NULL 为丢失的叶子返回 null。
  • 即使路径是确定的,Option.ALWAYS_RETURN_LIST 也会返回一个列表。
  • Option.SUPPRESS_EXCEPTIONS 确保没有异常从路径评估中传播。
  • Option.REQUIRE_PROPERTIES 需要在评估不确定路径时在路径中定义的属性。

以下是如何从头开始应用Option

Configuration configuration = Configuration.builder().options(Option.<OPTION>).build();

以及如何将其添加到现有配置中:

Configuration newConfiguration = configuration.addOptions(Option.<OPTION>);

5.2. SPI

JsonPath 在Option的帮助下的默认配置应该足以完成大多数任务。但是,具有更复杂用例的用户可以根据自己的特定要求修改 JsonPath 的行为——使用三种不同的 SPI:

  • JsonProvider SPI 让我们改变 JsonPath 解析和处理 JSON 文档的方式。
  • MappingProvider SPI 允许自定义节点值和返回的对象类型之间的绑定。
  • CacheProvider SPI 调整缓存路径的方式,有助于提高性能。

6. 示例用例

我们现在对 JsonPath 功能有了很好的理解。那么,让我们看一个例子。

本节说明如何处理从 Web 服务返回的 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是电影院电影的美元收入。

我们将处理与 GET 请求相关的五种不同工作场景,假设上述 JSON 层次结构已被提取并存储在名为jsonStringstring变量中。

6.1.获取给定 ID 的对象数据

在这个用例中,客户端通过向服务器提供电影的确切id来请求有关特定电影的详细信息。此示例演示服务器如何在返回客户端之前查找请求的数据。

假设我们需要找到一个id等于 2 的记录。

第一步是选择正确的数据对象:

Object dataObject = JsonPath.parse(jsonString).read("$[?(@.id == 2)]");
String dataString = dataObject.toString();

JUnit Assert API 确认了几个字段的存在:

assertThat(dataString, containsString("2"));
assertThat(dataString, containsString("Quantum of Solace"));
assertThat(dataString, containsString("Twenty-second James Bond movie"));

6.2. 获取给定主演的电影名称

假设我们要查找由女演员Eva Green主演的电影。服务器需要返回在starring 数组中包含Eva Green的电影的*title *。

随后的测试将说明如何执行此操作并验证返回的结果:

@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. 总收入的计算

此场景使用名为*length()*的 JsonPath 函数来计算电影记录的数量,以计算所有电影的总收入。

让我们看一下实现和测试:

@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. 最高收入电影

这个用例举例说明了使用非默认 JsonPath 配置选项,即Option.AS_PATH_LIST来找出收入最高的电影。

首先,我们需要提取所有电影票房收入的列表。然后我们将其转换为数组进行排序:

DocumentContext context = JsonPath.parse(jsonString);
List<Object> revenueList = context.read("$[*]['box office']");
Integer[] revenueArray = revenueList.toArray(new Integer[0]);
Arrays.sort(revenueArray);

我们可以轻松地从incomeArray 排序数组中取出最高收入变量,然后用它计算出收入最高的电影记录的路径:

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 + ")]");

根据计算出的路径,我们将确定并返回相应电影的title

Map<String, String> dataRecord = context.read(pathList.get(0));
String title = dataRecord.get("title");

整个过程通过Assert API 进行验证:

assertEquals("Skyfall", title);

6.5. 导演的最新电影

这个例子将说明如何计算出导演Sam Mendes导演的最后一部电影。

首先,我们创建一个包含Sam Mendes导演的所有电影的列表:

DocumentContext context = JsonPath.parse(jsonString);
List<Map<String, Object>> dataList = context.read("$[?(@.director == 'Sam Mendes')]");

然后,我们使用该列表来提取发布日期。这些日期将存储在一个数组中,然后排序:

List<Object> dateList = new ArrayList<>();
for (Map<String, Object> item : dataList) {
    Object date = item.get("release date");
    dateList.add(date);
}
Long[] dateArray = dateList.toArray(new Long[0]);
Arrays.sort(dateArray);

我们使用lastestTime变量(排序数组的最后一个元素)结合director字段的值来确定请求电影的title

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);