Jsonpath 算是很基本的技术工具,不过我过去主要是在 PostgreSQL 数据库中使用。近期的工作中要在 Java 层面处理一些复杂的 Json 结构,于是找到了一个 JsonPath 工具库 https://github.com/json-path/JsonPath

在这里不多讨论 Jsonpath 的语法了,可以参考各种相关的文档,需要注意的是,因为 Jsonpath 是一个相对没那么热门的概念,所以不同的 Jsonpath 解释器实现的功能会有些细节上的区别,要经过测试才能确定使用。

这个库的使用体验非常好,提供了不同灵活性和复杂度的接口。不过有些细节还是比较单薄的,后面我们具体情况具体讨论。这里不多展开它文档上着重介绍的那些,介绍一些相对来说不容易第一时间发现的。

基本用法

如果我们只是简单的从 JSON 文本中提取指定路径的内容,那么类似下面这个例子就足够用了。

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

需要注意的是,查询中有些位置是不能有空格的,比如正则表达式。这个我没有在文档里看到,但是实测确实会出问题。好在正则表达式中我们可以用 \s 代表空格。需要注意的是,根据正则表达式所在的内容深度,要记得补全对应的转义符。也就是说,我们实际要写作 \\s:

JsonPath.read(json, "$..book[?(@.author =~ /^J\\.\\s.*?/i)]");
//  [{"category":"fiction","author":"J. R. R. Tolkien","title":"The Lord of the Rings","isbn":"0-395-19395-8","price":22.99}]

这里有个细节,就是它会自动转换 List(对应 Json Array),Map 和 Java 基本类型以及文本对应的那些类型,如果需要更复杂的类型映射,可以参考后面的内容。

预编译查询

对于反复使用的 Jsonpath 查询,我们可以预先编译为一个 JsonPath

JsonPath query =JsonPath.compile("$.data.items");
List<String> books = query.read(content);

预编译文档

反过来说,重复使用的文档,也可以预处理:

DocumentContext doc =JsonPath.parse(content);
 Integer size = doc.read("$.data.items.length()");

类型映射

前面说过,对于基本类型和结构,JsonPath 可以直接处理,那么我们的自定义类型呢?其实是可以通过配置 JsonProvider 和 MapProvider 来解决。这个机制还是相当透明的。默认情况下,它使用自己的 JsonProvider 和 MapProvider ,但是也支持持用 Jackson 和 Gson :

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.TypeRef;
import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
// ...
Configuration jsonPathConfig =
                Configuration.builder()
                        .jsonProvider(newJacksonJsonProvider(config.objectMapper()))
                        .mappingProvider(newJacksonMappingProvider(config.objectMapper()))
                        .build();
finalTypeRef<List<ContentItem>> itemsRef =newTypeRef<List<ContentItem>>() {
    };
// ...
List<ContentItem> items =JsonPath
                            .using(jsonPathConfig)
                            .parse(content)
                            .read("$.data.items", itemsRef);

这是我在工作中使用的一段测试代码,content 的实际内容和 ContentItem 的内部结构我们不需要关心,重点在于我们通过 TypeRef 绕过了擦拭法,将具体的列表元素类型传递给了 JsonPath 程序。这里我用的是 Jackson,Gson 用户换对应的设置即可。从代码实现上看,这些 Provider 并不复杂,FastJson 等未支持的 Json 库,用户自己写出对应的 Provider 也并不困难。

需要注意的有两处,一个是我们必须同时提供 JsonProvider 和 MapProvider,否则它还是会调用内置的 JsonSmartJsonProvider 。然后在构造复杂类型时候报错:

jsonPathConfig =
                Configuration.builder()
                        .jsonProvider(newJacksonJsonProvider(config.objectMapper()))
                        //.mappingProvider(new JacksonMappingProvider(config.objectMapper()))
                        .build();
// ...
List<ContentItem> items =JsonPath
                            .using(jsonPathConfig)
                            .parse(content)
                            .read("$.data.items", itemsRef);
//java.lang.UnsupportedOperationException: Json-smart provider does not support TypeRef! Use a Jackson or Gson based provider

这个例子中,我注释了 JacksonMappingProvider 的配置,但是抛出的异常提示的是 JsonProvider 问题。这里的信息是有点儿误导的。

这里还有一个细节是,构造 JacksonMappingProvider 的时候,我们可以传递自己的 ObjectMapper 进去,这就可以让我们统一配置 JsonPath 和 Jackson 的处理行为。

安装

JsonPath 的安装没有什么特殊的,正常的 maven 库:

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