跳到正文
返回

JSONPath 快速入门

发表于 更新于
浏览量: --

封面来源:由 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 中:

<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.7.0</version>
</dependency>

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

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

$.store.book[0].title

或者 括号表示法

$['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 为负数,从后面取依赖于数组
//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 信息:

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

一些使用示例

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,比如:

String json = "...";

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

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

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

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:

@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 将自动尝试将结果转换为调用方所期望的结果:

@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 是不确定的:

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

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

@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 中:

@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

@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 中定义的谓词。

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

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

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

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

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

@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 来创建谓词,比如:

@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

还可以自定义过滤器:

@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,可以通过一个选项来实现:

@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

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

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

10.1 Tweaking Configuration

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

本节使用到的 JSON 信息:

[
  {
    "name" : "john",
    "gender" : "male"
  },
  {
    "name" : "ben"
  }
]

10.2 DEFAULT_PATH_LEAF_TO_NULL

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

@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"));
}

10.3 ALWAYS_RETURN_LIST

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

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

10.4 SUPPRESS_EXCEPTIONS

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

@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());
}

10.5 REQUIRE_PROPERTIES

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

@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");
    });
}

11. JsonProvider SPI

JSONPath 提供了五种不同的 JsonProvider

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

比如使用 JacksonJsonProvider 作为默认 JsonProvider

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 的默认配置值。

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

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

12. Cache SPI

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

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

提供了两种缓存实现:

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

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 缓存:

@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"));
}
如果这篇文章对你有帮助,可以通过
支付宝
支付宝
微信
微信
请我喝杯 Coffee ☕


上一篇
从回调到 LambdaSafe
下一篇
【MySQL 高级篇二】MySQL 的数据目录