封面来源:由 bestony/logoly: A Pornhub Flavour Logo Generator 绘制。

GitHub:json-path/JsonPath: Java JsonPath implementation

PS:本文内容由以上 GitHub 仓库的 README 文件内容翻译而来。

本文涉及的代码:mofan-demo/JsonPathTest.java

1. Getting Started

JSONPath 同 JSON 的关系,正如 XPath 与 XML 的关系一样。JSONPath 被设计来作为 JSON 的路径语言,用于确定 JSON 文档中某部分位置的语言。

本文选用的 JSONPath 实现基于 Java 语言。

Maven 用户可将如下坐标导入 POM 中:

1
2
3
4
5
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.7.0</version>
</dependency>

JSONPath 表达式总是引用 JSON 结构,就像 XPath 表达式与 XML 结合使用一样。JSONPath 中的“根成员对象”总是被称为 $,无论是对象还是数组。

JSONPath 表达式可以使用 点表示法

1
$.store.book[0].title

或者 括号表示法

1
$['store']['book'][0]['title']

2. Operators

操作符 描述
$ 要查询的根元素,是所有表达式的开始
@ 通过过滤器谓词处理当前节点
* 通配符,可在任何需要名称或数字的地方使用
.. 深度扫描,可在任何需要名称的地方使用
.<name> 用点表示单个子节点
['<name>' (, '<name>')] 用括号表示单个或多个子节点
[<number> (, <number>)] 数组索引
[start:end] 数组切片操作
[?(<expression>)] 过滤器表达式,计算结果必须是布尔值

3. Functions

函数可以在 JSONPath 的尾部被调用,函数的输入是 Path 的输出,函数的输出由函数本身决定。

函数 描述 输出类型
min() 提供数字数组的最小值 Double
max() 提供数字数组的最大值 Double
avg() 提供数字数组的平均值 Double
stddev() 提供数字数组的标准偏差值 Double
length() 提供数组的长度 Integer
sum() 提供数字数组的和 Double
keys() 提供属性键值集合 Set<E>
concat(X) 将输出的 JSON 信息按指定拼接符进行拼接 like input
append(X) 向输出的 JSON 数组中添加一项 like input
first() 提供 JSON 数组的第一项 依赖于数组
last() 提供 JSON 数组的最后一项 依赖于数组
index(X) 提供 JSON 数组中索引为 X 的项,如果 X 为负数,从后面取 依赖于数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//language=JSON
static final String FUNC_JSON = "{\n" +
" \"text\": [\"A\", \"B\"],\n" +
" \"nums\": [1, 2]\n" +
"}";

@Test
public void testConcatFunc() {
DocumentContext context = JsonPath.parse(FUNC_JSON);

String value = context.read("$.text.concat()");
Assertions.assertEquals("AB", value);

value = context.read("$.concat($.text[0], $.nums[0])");
Assertions.assertEquals("A1", value);

value = context.read("$.text.concat(\"-\", \"CD\")");
Assertions.assertEquals("AB-CD", value);
}

@Test
public void testAppendFunc() {
DocumentContext context = JsonPath.parse(FUNC_JSON);

// 1 + 2 + 3 + 4
double sum = context.read("$.nums.append(3, 4).sum()");
Assertions.assertEquals(10.0, sum);

// 1 + 2 + 3 + 4 + 5 + 6
sum = context.read("$.nums.append(\"3\", \"4\").sum(5, 6)");
Assertions.assertEquals(21, sum);
}

@Test
public void testIndexFunc() {
DocumentContext context = JsonPath.parse(FUNC_JSON);

Integer value = context.read("$.nums[0]");
Assertions.assertEquals(1, value);

value = context.read("$.nums[-1]");
Assertions.assertEquals(2, value);

Assertions.assertThrowsExactly(PathNotFoundException.class, () -> {
context.read("$.nums[2]");
});
}

4. Filter Operators

过滤器是用于筛选数组的逻辑表达式,比如 [?(@.age > 18)],其中 @ 表示正在处理的当前项。

可以使用逻辑运算符 &&|| 创建更复杂的过滤器。

字符串必须用单引号或双引号括起来,比如 ([?(@.color == 'blue')][?(@.color == "blue")])

操作符 描述
== 左值等于右值(注意 1 不等于 '1'
!= 左值不等于右值
< 左值小于右值
<= 左值小于等于右值
> 左值大于右值
>= 左值大于等于右值
=~ 匹配正则表达式,比如 [?(@.name =~ /foo.*?/i)]
in 左值存在于右值中,比如 [?(@.size in ['S', 'M'])]
nin 左值不存在于右值中
subsetof 左值是右值的子集,比如 [?(@.sizes subsetof ['S', 'M', 'L'])]
anyof 左值与右值有交集,比如 [?(@.sizes anyof ['M', 'L'])]
noneof 左值与右值没有交集,比如 [?(@.sizes noneof ['M', 'L'])]
size 左值(数组或字符串)的 size 与右值匹配
empty 左值(数组或字符串)为空

5. Path Examples

给定一段 JSON 信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}

一些使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
private static final String JSON = FileUtil.readString("json-path.json", Charset.defaultCharset());

private static final Object DOCUMENT = Configuration.defaultConfiguration().jsonProvider().parse(JSON);

@Test
public void testPathExample() {
// store 下所有 book 列表的 author 信息
List<String> authors = JsonPath.read(DOCUMENT, "$.store.book[*].author");
Assertions.assertEquals(4, authors.size());

// JSON 数据中所有的 author 值
authors = JsonPath.read(DOCUMENT, "$..author");
Assertions.assertEquals(4, authors.size());

// store 下的所有信息(返回的是一个列表,而不是 Map)
List<Object> all = JsonPath.read(DOCUMENT, "$.store.*");
Assertions.assertEquals(2, all.size());
Assertions.assertTrue(all.get(0) instanceof List);
List<?> allBooks = (List<?>) all.get(0);
Assertions.assertEquals(4, allBooks.size());

// store 下所有 price
List<Double> prices = JsonPath.read(DOCUMENT, "$.store..price");
// 还有 bicycle 下的 price
Assertions.assertEquals(5, prices.size());

// 获取 book 数组的第三个值
List<Map<String, Object>> thirdBooks = JsonPath.read(DOCUMENT, "$..book[2]");
Assertions.assertEquals(1, thirdBooks.size());
Map<String, Object> thirdBook = thirdBooks.get(0);
Assertions.assertEquals(5, thirdBook.size());
Assertions.assertEquals("0-553-21311-3", thirdBook.get("isbn"));

// 获取 book 数据的倒数第二本书
List<Map<String, Object>> books = JsonPath.read(DOCUMENT, "$..book[-2]");
Assertions.assertEquals(1, books.size());
Map<String, Object> penultimateBook = books.get(0);
Assertions.assertEquals(5, penultimateBook.size());
Assertions.assertEquals("0-553-21311-3", thirdBook.get("isbn"));

// 前两本书
books = JsonPath.read(DOCUMENT, "$..book[0, 1]");
Assertions.assertEquals(2, books.size());
Assertions.assertEquals("reference", books.get(0).get("category"));
Assertions.assertEquals("fiction", books.get(1).get("category"));

// 前两本书
books = JsonPath.read(DOCUMENT, "$..book[:-2]");
Assertions.assertEquals("reference", books.get(0).get("category"));
Assertions.assertEquals("fiction", books.get(1).get("category"));

// 索引 [0, 2) 的树
books = JsonPath.read(DOCUMENT, "$..book[:2]");
Assertions.assertEquals(2, books.size());
Assertions.assertEquals("reference", books.get(0).get("category"));
Assertions.assertEquals("fiction", books.get(1).get("category"));

// 索引 [1, 2) 的树
books = JsonPath.read(DOCUMENT, "$..book[1:2]");
Assertions.assertEquals(1, books.size());
Assertions.assertEquals("fiction", books.get(0).get("category"));

// 最后两本书
books = JsonPath.read(DOCUMENT, "$..book[-2:]");
Assertions.assertEquals(2, books.size());
boolean allMatch = books.stream().map(i -> String.valueOf(i.get("category"))).allMatch("fiction"::equals);
Assertions.assertTrue(allMatch);

// 获取从第三本书开始(包括第三本)的所有数据
books = JsonPath.read(DOCUMENT, "$..book[2:]");
Assertions.assertEquals(2, books.size());

// 书籍信息中包含 isbn 的书
books = JsonPath.read(DOCUMENT, "$..book[?(@.isbn)]");
Assertions.assertEquals(2, books.size());

// 书籍信息中 price < 10 的所有书
books = JsonPath.read(DOCUMENT, "$..book[?(@.price < 10)]");
Assertions.assertEquals(2, books.size());

// 使用原始 JSON 数据中的值进行过滤
books = JsonPath.read(DOCUMENT, "$..book[?(@.price < $['expensive'])]");
Assertions.assertEquals(2, books.size());

// 获取书籍信息中的 author 以 REES 结尾的所有书(REES 不区分大小写)
books = JsonPath.read(DOCUMENT, "$..book[?(@.author =~ /.*REES/i)]");
Assertions.assertEquals(1, books.size());

// JSON 中所有值
List<Object> value = JsonPath.read(DOCUMENT, "$..*");
Assertions.assertTrue(value.contains("Herman Melville"));
Assertions.assertTrue(value.stream().anyMatch(i -> Map.class.isAssignableFrom(i.getClass())));

// book 的数量
int number = JsonPath.read(DOCUMENT, "$..book.length()");
Assertions.assertEquals(4, number);
}

6. Reading a Document

最简单、最直接使用 JSONPath 的方式是使用静态读取 API,比如:

1
2
3
String json = "...";

List<String> authors = JsonPath.read(json, "$.store.book[*].author");

如果对目标 JSON 数据只读取一次,上述读取方式是可行的。

如果还需要读取其他 Path 的信息,那么上述方式并不是最优的,因为每次调用 JsonPath.read() 时都需要解析 JSON 文本,这会带来额外的性能消耗。最优的做法是只解析一次 JSON 文本,然后使用解析结果读取其他 Path 的信息:

1
2
3
4
5
String json = "...";
Object document = Configuration.defaultConfiguration().jsonProvider().parse(json);

String author0 = JsonPath.read(document, "$.store.book[0].author");
String author1 = JsonPath.read(document, "$.store.book[1].author");

JSONPath 还提供了流式 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testReadADocumentWithFluentApi() {
// 不使用 fluent api
DocumentContext ctx = JsonPath.parse(JSON);
List<String> authorsOfBooksWithISBN = ctx.read("$.store.book[?(@.isbn)].author");
Assertions.assertEquals(2, authorsOfBooksWithISBN.size());

// 使用 fluent api
List<Map<String, Object>> expensiveBooks = JsonPath.using(Configuration.defaultConfiguration())
.parse(JSON)
.read("$.store.book[?(@.price > 10)]");
Assertions.assertEquals(2, expensiveBooks.size());
}

7. What is Returned When?

在 Java 中使用 JSONPath 时,最重要的是确定最终的返回值类型,JSONPath 将自动尝试将结果转换为调用方所期望的结果:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testWhatIsReturnedWhen() {
DocumentContext context = JsonPath.parse(JSON);

Assertions.assertThrowsExactly(ClassCastException.class, () -> {
List<String> list = context.read("$.store.book[0].author");
});

String author = context.read("$.store.book[0].author");
Assertions.assertEquals("Nigel Rees", author);
}

在计算 Path 时,需要理解 Path 是何时确定的概念。当包含以下表达式时,Path 是不确定的:

  • ..:深度扫描操作符
  • ?(<expression>):过滤器表达式
  • [<number>, <number> (, <number>)]:多个数组索引

不确定的 Path 总是返回一个列表(由当前的 JsonProvider 表示)。

默认情况下,MappingProvider SPI 提供了一个简单的对象映射器。这允许用户指定用户想要的返回类型,MappingProvider 将尝试执行映射。下面的示例演示了 LongDate 之间的映射。

1
2
3
4
5
6
7
@Test
public void testMappingLongToDate() {
String json = "{\"date_as_long\" : 1678377600000}";

Date date = JsonPath.parse(json).read("$['date_as_long']", Date.class);
Assertions.assertEquals("2023-03-10", new SimpleDateFormat("yyyy-MM-dd").format(date));
}

如果配置 JSONPath 使用 JacksonMappingProviderGsonMappingProvider 或者 JakartaJsonProvider,甚至可以将输出结果直接映射到 POJO 中:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testMappingResultToPojoOrGeneric() {
Configuration conf = Configuration.builder()
.jsonProvider(new JacksonJsonProvider())
.mappingProvider(new JacksonMappingProvider())
.options(EnumSet.noneOf(Option.class))
.build();

DocumentContext context = JsonPath.using(conf).parse(JSON);
Book book = context.read("$.store.book[0]", Book.class);
Assertions.assertEquals("reference", book.getCategory());
}

获取完整的泛型类型信息,可以使用 TypeRef

1
2
3
4
5
6
7
8
@Test
public void testMappingResultToPojoOrGeneric() {
// --snip--

TypeRef<List<String>> typeRef = new TypeRef<List<String>>() {};
List<String> titles = context.read("$.store.book[*].title", typeRef);
Assertions.assertEquals(4, titles.size());
}

8. Predicates

在 JSONPath 中有三种不同的方式来创建过滤器谓词。

8.1 Inline Predicates

内联谓词是在 Path 中定义的谓词。

1
2
List<Map<String, Object>> books =  
JsonPath.parse(JSON).read("$.store.book[?(@.price < 10)]");

可以使用 &&|| 连接多个谓词:

1
2
3
[?(@.price < 10 && @.category == 'fiction')]

[?(@.category == 'reference' || @.price > 10)]

还可以使用 ! 为一个谓词取反:[?(!(@.price < 10 && @.category == 'fiction'))]

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testPredicates() {
// inlines predicates
List<Map<String, Object>> books = JsonPath.read(JSON, "$..book[?(@.price < 10 && @.category == 'fiction')]");
Assertions.assertEquals(1, books.size());
Assertions.assertEquals("Herman Melville", books.get(0).get("author"));
books = JsonPath.read(JSON, "$..book[?(@.category == 'reference' || @.price > 10)]");
Assertions.assertEquals(3, books.size());
books = JsonPath.read(JSON, "$..book[?(!(@.price < 10 && @.category == 'fiction'))]");
Assertions.assertEquals(3, books.size());
}

8.2 Filter Predicates

可以使用 Filter API 来创建谓词,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testPredicates() {
// --snip--

// filter predicates
Filter filter = Filter.filter(
Criteria.where("category").is("fiction").and("price").lte(10D)
);
books = JsonPath.read(JSON, "$..book[?]", filter);
Assertions.assertEquals(1, books.size());
Assertions.assertEquals("0-553-21311-3", books.get(0).get("isbn"));
Filter fooOrBar = Filter.filter(Criteria.where("foo").exists(true)).or(Criteria.where("bar").exists(true));
books = JsonPath.read(JSON, "$..book[?]", fooOrBar);
Assertions.assertTrue(books.isEmpty());
Filter fooAndBar = Filter.filter(Criteria.where("foo").exists(true)).and(Criteria.where("bar").exists(true));
books = JsonPath.read(JSON, "$..book[?]", fooAndBar);
Assertions.assertTrue(books.isEmpty());
}

注意: 占位符 ? 表示 Path 中的过滤器。当提供多个过滤器时,它们将按顺序被应用,其中占位符的数量必须与提供的过滤器数量相等。可以在一个过滤操作中指定多个谓词占位符,它们依次与传入的过滤器匹配。

8.3 Roll Your Own

还可以自定义过滤器:

1
2
3
4
5
6
7
8
9
@Test
public void testPredicates() {
// --snip--

// roll your own
Predicate predicate = ctx -> ctx.item(Map.class).containsKey("isbn");
books = JsonPath.read(JSON, "$..book[?]", predicate);
Assertions.assertEquals(2, books.size());
}

9. Path vs Value

JsonPath 可以返回 Path 或 Value。Value 是默认值,上文所有示例都是返回的 Value。如果想要查询元素的 Path,可以通过一个选项来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Test
public void testReturnPath() {
Configuration conf = Configuration.builder()
.options(Option.AS_PATH_LIST)
.build();
String jsonPath = "$..author";
ParseContext parseContext = JsonPath.using(conf);
List<String> pathList = parseContext
.parse(JSON)
.read(jsonPath);
List<String> correctPathList = Arrays.asList(
"$['store']['book'][0]['author']",
"$['store']['book'][1]['author']",
"$['store']['book'][2]['author']",
"$['store']['book'][3]['author']"
);
Assertions.assertIterableEquals(pathList, correctPathList);

// read document
DocumentContext context = JsonPath.parse(JSON);
Assertions.assertThrowsExactly(PathNotFoundException.class,
() -> parseContext.parse(context).read(jsonPath));

pathList = parseContext.parse((Object) context.json()).read(jsonPath);
Assertions.assertIterableEquals(pathList, correctPathList);

Object document = conf.jsonProvider().parse(JSON);
pathList = parseContext.parse(document).read(jsonPath);
Assertions.assertIterableEquals(pathList, correctPathList);
}

如果需要只解析一次 JSON 信息,不能将 JsonPath.parse() 的解析结果 DocumentContext 对象 直接 传入 ParseContext#parse() 方法中,这将抛出 PathNotFoundException 异常,而是应该传入 Configuration#jsonProvider()#parse() 的解析结果,或者传入调用 DocumentContext 对象的 json() 方法返回的对象(这就是用 Object 类型作为参数的弊端!)。

10. Set a value

当前使用的库提供了设置值的可能:

1
2
3
4
5
6
7
8
9
@Test
public void testSetAValue() {
String newJson = JsonPath.parse(JSON)
.set("$.store.book[0].author", "Paul")
.jsonString();

String firstBookAuthor = JsonPath.parse(newJson).read("$.store.book[0].author", String.class);
Assertions.assertEquals("Paul", firstBookAuthor);
}

11. Tweaking Configuration

在创建 Configuration 对象时,提供了一些选项来改变默认行为。

本节使用到的 JSON 信息:

1
2
3
4
5
6
7
8
9
[
{
"name" : "john",
"gender" : "male"
},
{
"name" : "ben"
}
]

11.1 DEFAULT_PATH_LEAF_TO_NULL

该选项使得 JSONPath 在缺少叶子节点时总是返回 null,而不是抛出 PathNotFoundException 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testDefaultPathLeafToNull() {
Configuration conf = Configuration.defaultConfiguration();
String gender0 = JsonPath.using(conf).parse(TEST_JSON).read("$.[0].gender");
Assertions.assertEquals("male", gender0);
Assertions.assertThrowsExactly(PathNotFoundException.class, () -> {
JsonPath.using(conf).parse(TEST_JSON).read("$.[1].gender");
});

// add DEFAULT_PATH_LEAF_TO_NULL option
Configuration conf2 = conf.addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL);
gender0 = JsonPath.using(conf2).parse(TEST_JSON).read("$.[0].gender");
Assertions.assertEquals("male", gender0);
Assertions.assertNull(JsonPath.using(conf2).parse(TEST_JSON).read("$.[1].gender"));
}

11.2 ALWAYS_RETURN_LIST

该选项使得 JSONPath 总是返回一个 List,即便 Path 是确定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void testAlwaysReturnList() {
Configuration conf = Configuration.defaultConfiguration();
Object gender0 = JsonPath.using(conf).parse(TEST_JSON).read("$.[0].gender");
Assertions.assertEquals("male", gender0);
Assertions.assertThrowsExactly(ClassCastException.class, () -> {
@SuppressWarnings("unchecked")
List<String> list = (List<String>) gender0;
});

Configuration conf2 = conf.addOptions(Option.ALWAYS_RETURN_LIST);
Object gender2 = JsonPath.using(conf2).parse(TEST_JSON).read("$.[0].gender");
Assertions.assertTrue(gender2 instanceof List);
}

11.3 SUPPRESS_EXCEPTIONS

该选项使得 JSONPath 在解析过程中不会抛出异常,遵循以下规则:

  • 配置了 ALWAYS_RETURN_LIST 时,总是返回空列表;
  • 未配置 ALWAYS_RETURN_LIST 时,总是返回 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testSuppressExceptions() {
Configuration conf = Configuration.defaultConfiguration();
Assertions.assertThrowsExactly(PathNotFoundException.class, () -> {
JsonPath.using(conf).parse(TEST_JSON).read("$.[1].gender");
});

Configuration conf2 = conf.addOptions(Option.SUPPRESS_EXCEPTIONS);
Object gender1 = JsonPath.using(conf2).parse(TEST_JSON).read("$.[1].gender");
Assertions.assertNull(gender1);

Configuration conf3 = conf2.addOptions(Option.ALWAYS_RETURN_LIST);
gender1 = JsonPath.using(conf3).parse(TEST_JSON).read("$.[1].gender");
Assertions.assertTrue(gender1 instanceof List);
Assertions.assertTrue(((List<?>) gender1).isEmpty());
}

11.4 REQUIRE_PROPERTIES

该选项要求 JSONPath 解析的 Path 中的每个属性都必须存在,否则抛出 PathNotFoundException 异常。

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testRequireProperties() {
Configuration conf = Configuration.defaultConfiguration();
List<String> genders = JsonPath.using(conf).parse(TEST_JSON).read("$.[*].gender");
Assertions.assertEquals(1, genders.size());
Assertions.assertEquals("male", genders.get(0));

Configuration conf2 = conf.addOptions(Option.REQUIRE_PROPERTIES);
Assertions.assertThrowsExactly(PathNotFoundException.class, () -> {
JsonPath.using(conf2).parse(TEST_JSON).read("$.[*].gender");
});
}

12. JsonProvider SPI

JSONPath 提供了五种不同的 JsonProvider

只有在初始化应用程序时才能更改 默认 配置值。强烈反对 在运行时进行更改,尤其是在多线程的应用程序中。

比如使用 JacksonJsonProvider 作为默认 JsonProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Configuration.setDefaults(new Configuration.Defaults() {

private final JsonProvider jsonProvider = new JacksonJsonProvider();
private final MappingProvider mappingProvider = new JacksonMappingProvider();

@Override
public JsonProvider jsonProvider() {
return jsonProvider;
}

@Override
public MappingProvider mappingProvider() {
return mappingProvider;
}

@Override
public Set<Option> options() {
return EnumSet.noneOf(Option.class);
}
});

在 SpringBoot 项目中,可以实现 ApplicationRunner 或者 CommandLineRunner 接口,并在重写的 run() 方法中添加上述配置,使得在容器启动成功后修改 JSONPath 的默认配置值。

还可以监听 ApplicationReadyEvent 或者 ApplicationStartedEvent 事件,在监听器方法中修改 JSONPath 的默认配置值。

简单说下这些接口和区别:

  • ApplicationRunnerCommandLineRunner 基本没有区别,仅仅是其内部的抽象方法参数不一样;
  • ApplicationReadyEventApplicationStartedEvent 的区别也不大,ApplicationStartedEventApplicationReadyEvent 而言更先执行,而 ApplicationRunner 则是在它们之间被执行。

所以无论是实现上述接口,还是监听上述事件,都能达到在容器启动成功后修改 JSONPath 的默认配置值。

13. Cache SPI

在 2.1.0 版本中引入了 Cache SPI,这允许 API 使用者以适合他们需求的方式配置 Path 缓存。

缓存必须在第一次使用 API 或者抛出 JsonPathException 之前进行配置。

提供了两种缓存实现:

  • com.jayway.jsonpath.spi.cache.LRUCache:默认实现,线程安全
  • com.jayway.jsonpath.spi.cache.NOOPCache:无缓存

自定义缓存实现也很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CacheProvider.setCache(new Cache() {
// Not thread safe simple cache
private Map<String, JsonPath> map = new HashMap<String, JsonPath>();

@Override
public JsonPath get(String key) {
return map.get(key);
}

@Override
public void put(String key, JsonPath jsonPath) {
map.put(key, jsonPath);
}
});

测试下默认的 LRU 缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
@SneakyThrows
public void testCacheSpi() {
List<String> names = JsonPath.read(TEST_JSON, "$.[*].name");
Assertions.assertEquals(2, names.size());
Cache cache = CacheProvider.getCache();
Assertions.assertTrue(cache instanceof LRUCache);
LRUCache lruCache = (LRUCache) cache;
Assertions.assertTrue(lruCache.size() >= 1);
Field mapField = lruCache.getClass().getDeclaredField("map");
mapField.setAccessible(true);
@SuppressWarnings("unchecked")
ConcurrentHashMap<String, ?> concurrentHashMap = (ConcurrentHashMap<String, ?>) mapField.get(lruCache);
Assertions.assertTrue(concurrentHashMap.containsKey("$.[*].name"));
}