封面来源:由 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
| 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);
double sum = context.read("$.nums.append(3, 4).sum()"); Assertions.assertEquals(10.0, sum);
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() { List<String> authors = JsonPath.read(DOCUMENT, "$.store.book[*].author"); Assertions.assertEquals(4, authors.size());
authors = JsonPath.read(DOCUMENT, "$..author"); Assertions.assertEquals(4, authors.size());
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());
List<Double> prices = JsonPath.read(DOCUMENT, "$.store..price"); Assertions.assertEquals(5, prices.size());
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"));
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"));
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"));
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());
books = JsonPath.read(DOCUMENT, "$..book[?(@.isbn)]"); Assertions.assertEquals(2, books.size());
books = JsonPath.read(DOCUMENT, "$..book[?(@.price < 10)]"); Assertions.assertEquals(2, books.size());
books = JsonPath.read(DOCUMENT, "$..book[?(@.price < $['expensive'])]"); Assertions.assertEquals(2, books.size());
books = JsonPath.read(DOCUMENT, "$..book[?(@.author =~ /.*REES/i)]"); Assertions.assertEquals(1, books.size());
List<Object> value = JsonPath.read(DOCUMENT, "$..*"); Assertions.assertTrue(value.contains("Herman Melville")); Assertions.assertTrue(value.stream().anyMatch(i -> Map.class.isAssignableFrom(i.getClass())));
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() { DocumentContext ctx = JsonPath.parse(JSON); List<String> authorsOfBooksWithISBN = ctx.read("$.store.book[?(@.isbn)].author"); Assertions.assertEquals(2, authorsOfBooksWithISBN.size());
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() {
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() { 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() {
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() {
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);
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"); });
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() { 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")); }
|