封面来源:由 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']
|
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 将尝试执行映射。下面的示例演示了 Long 和 Date 之间的映射。
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 使用 JacksonMappingProvider、GsonMappingProvider 或者 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 的默认配置值。
简单说下这些接口和区别:
ApplicationRunner 和 CommandLineRunner 基本没有区别,仅仅是其内部的抽象方法参数不一样;
ApplicationReadyEvent 和 ApplicationStartedEvent 的区别也不大,ApplicationStartedEvent 较 ApplicationReadyEvent 而言更先执行,而 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"));
}
|