1. 概述
XML 的優勢之一在於其處理能力的可獲得性——包括 XPath,後者被定義為 W3C 標準。對於 JSON,出現了一種類似的工具,稱為 JSONPath。
本教程將介紹 Jayway JsonPath,這是一種 Java 實現的 JSONPath 規範。它描述了設置、語法、常用 API 以及使用案例的演示。
2. 安裝配置
要使用 JsonPath,只需在 Maven pom 中添加一個依賴項即可:
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.9.0</version>
</dependency>3. 語法
我們將使用以下 JSON 結構來演示 JsonPath 的語法和 API:
{
"tool":
{
"jsonpath":
{
"creator":
{
"name": "Jayway Inc.",
"location":
[
"Malmo",
"San Francisco",
"Helsingborg"
]
}
}
},
"book":
[
{
"title": "Beginning JSON",
"price": 49.99
},
{
"title": "JSON at Work",
"price": 29.99
}
]
}3.1. 符號表示
JsonPath 使用特殊符號表示節點及其與其相鄰節點的連接,這些連接構成 JsonPath 路徑。 符號表示有兩種樣式:點和方括號。
以下兩個路徑都指向 JSON 文檔中相同的節點,即在 location 字段中的第三個元素,該元素是 creator 節點的一個子節點,該子節點屬於根節點下的 tool 對象中的 jsonpath 對象。
首先,我們來看使用點符號表示的路徑:
$.tool.jsonpath.creator.location[2]現在讓我們來看一下花括號表示法:
$['tool']['jsonpath']['creator']['location'][2]美元符號($)表示根成員對象。
3.2. 運算符
我們有幾個有用的運算符用於 JsonPath:
- 根節點 ($) 表示 JSON 結構的根成員,無論它是對象還是數組。我們在上一節中包含了用法示例。
- 當前節點 (@) 表示正在處理的節點。我們通常將其作為輸入表達式中的一部分,用於謂詞。例如,如果我們在上述 JSON 文檔中處理 書數組,表達式 book[?(@.price == 49.99)]指的是該數組中的第一個 書。
- 通配符 (*) 表示指定範圍內所有元素。例如,book[*]表示book數組內的所有節點。
3.3 函數和過濾器
JsonPath 具有一些函數,我們可以將它們用於路徑末尾以合成路徑的輸出表達式:<em>min()</em>, <em>max()</em>, <em>avg()</em>, `stddev() 和 length()。
最後,我們有過濾器。這些是布爾表達式,用於限制返回的節點列表,僅限於調用方法所需的節點。
以下是一些示例:相等性 (<em>==</em>)、正則表達式匹配 (<em>=~</em>)、包含 (<em>in</em>) 和檢查空值 (<em>empty</em>)。我們主要使用過濾器進行謂詞。
對於完整的列表以及不同操作符、函數和過濾器的詳細解釋,請參閲 JsonPath GitHub 項目。
4. 運維
在開始討論運維之前,先做個簡短的説明:本部分使用了我們之前定義的 JSON 示例結構。
4.1. 訪問文檔
JsonPath 提供了一種便捷的方式來訪問 JSON 文檔。我們通過靜態的 read API 來實現這一點:
<T> T JsonPath.read(String jsonString, String jsonPath, Predicate... filters);讀取 API 可以與靜態流式 API 配合使用,從而提供更大的靈活性:
<T> T JsonPath.parse(String jsonString).read(String jsonPath, Predicate... filters);我們可以使用 read 的其他重載變體,以處理不同類型的 JSON 來源,包括 Object、InputStream、URL 和 File。
為了簡化問題,本部分測試中未包含參數列表中指定的謂詞(空 varargs)。但我們在後續子章節中將討論 predicates。
我們先定義兩個示例路徑進行測試:
String jsonpathCreatorNamePath = "$['tool']['jsonpath']['creator']['name']";
String jsonpathCreatorLocationPath = "$['tool']['jsonpath']['creator']['location'][*]";接下來,我們將通過解析給定的 JSON 數據源 jsonDataSourceString 創建一個 DocumentContext 對象。新創建的對象將被用於使用上述定義的路徑讀取內容:
DocumentContext jsonContext = JsonPath.parse(jsonDataSourceString);
String jsonpathCreatorName = jsonContext.read(jsonpathCreatorNamePath);
List<String> jsonpathCreatorLocation = jsonContext.read(jsonpathCreatorLocationPath);第一個 read API 返回一個 String,其中包含 JsonPath 創建器的名稱,而第二個 API 則返回其地址列表。
我們將會使用 JUnit 的 Assert API 來驗證方法是否按預期工作:
assertEquals("Jayway Inc.", jsonpathCreatorName);
assertThat(jsonpathCreatorLocation.toString(), containsString("Malmo"));
assertThat(jsonpathCreatorLocation.toString(), containsString("San Francisco"));
assertThat(jsonpathCreatorLocation.toString(), containsString("Helsingborg"));4.2. 謂詞
現在我們已經掌握了基本概念,接下來讓我們定義一個新的 JSON 示例,以便進行演示和説明如何創建和使用謂詞:
{
"book":
[
{
"title": "Beginning JSON",
"author": "Ben Smith",
"price": 49.99
},
{
"title": "JSON at Work",
"author": "Tom Marrs",
"price": 29.99
},
{
"title": "Learn JSON in a DAY",
"author": "Acodemy",
"price": 8.99
},
{
"title": "JSON: Questions and Answers",
"author": "George Duckett",
"price": 6.00
}
],
"price range":
{
"cheap": 10.00,
"medium": 20.00
}
}謂詞確定了真假輸入值,用於縮小返回列表,只包含匹配的對象或數組。我們可以輕鬆地將 Predicate 集成到 Filter 中,通過將其作為靜態工廠方法的參數使用。請求的內容可以使用該 Filter 從 JSON 字符串中讀取:
Filter expensiveFilter = Filter.filter(Criteria.where("price").gt(20.00));
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?]", expensiveFilter);
predicateUsageAssertionHelper(expensive);我們還可以定義自定義的 Predicate 並將其用作 read API 的參數:
Predicate expensivePredicate = new Predicate() {
public boolean apply(PredicateContext context) {
String value = context.item(Map.class).get("price").toString();
return Float.valueOf(value) > 20.00;
}
};
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?]", expensivePredicate);
predicateUsageAssertionHelper(expensive);最後,一個謂詞可以直接應用於 read API,而無需創建任何對象,這種方法稱為內聯謂詞。
List<Map<String, Object>> expensive = JsonPath.parse(jsonDataSourceString)
.read("$['book'][?(@['price'] > $['price range']['medium'])]");
predicateUsageAssertionHelper(expensive);所有三個 謂詞 示例均已通過以下斷言輔助方法驗證:
private void predicateUsageAssertionHelper(List<?> predicate) {
assertThat(predicate.toString(), containsString("Beginning JSON"));
assertThat(predicate.toString(), containsString("JSON at Work"));
assertThat(predicate.toString(), not(containsString("Learn JSON in a DAY")));
assertThat(predicate.toString(), not(containsString("JSON: Questions and Answers")));
}5. 配置
5.1. 選項
Jayway JsonPath 提供了一些選項來調整默認配置:
- Option.AS_PATH_LIST 返回評估命中點的路徑,而不是它們的實際值。
- Option.DEFAULT_PATH_LEAF_TO_NULL 對於缺失的葉節點返回 null。
- Option.ALWAYS_RETURN_LIST 即使路徑明確時,也返回一個列表。
- Option.SUPPRESS_EXCEPTIONS 確保路徑評估過程中不會傳播異常。
- Option.REQUIRE_PROPERTIES 在評估不明確路徑時,要求路徑中定義的屬性。
以下是如何從頭開始應用 Option 的方法:
Configuration configuration = Configuration.builder().options(Option.<OPTION>).build();以及如何將其添加到現有的配置中:
Configuration newConfiguration = configuration.addOptions(Option.<OPTION>);5.2. SPIs
JsonPath 的默認配置,藉助 Option 選項,通常足以滿足大部分任務的需求。然而,對於具有更復雜用例的用户,他們可以根據特定要求修改 JsonPath 的行為——通過三種不同的 SPIs 實現:
- JsonProvider SPI 允許我們更改 JsonPath 解析和處理 JSON 文檔的方式。
- MappingProvider SPI 允許自定義節點值和返回對象類型之間的綁定。
- CacheProvider SPI 調整路徑緩存的方式,這有助於提高性能。
6. 示例使用場景
我們現在對 JsonPath 的功能有了很好的理解。現在,讓我們來看一個示例。
本節將演示如何處理來自 Web 服務的 JSON 數據。
假設我們有一個電影信息服務,它返回以下結構:
[
{
"id": 1,
"title": "Casino Royale",
"director": "Martin Campbell",
"starring":
[
"Daniel Craig",
"Eva Green"
],
"desc": "Twenty-first James Bond movie",
"release date": 1163466000000,
"box office": 594275385
},
{
"id": 2,
"title": "Quantum of Solace",
"director": "Marc Forster",
"starring":
[
"Daniel Craig",
"Olga Kurylenko"
],
"desc": "Twenty-second James Bond movie",
"release date": 1225242000000,
"box office": 591692078
},
{
"id": 3,
"title": "Skyfall",
"director": "Sam Mendes",
"starring":
[
"Daniel Craig",
"Naomie Harris"
],
"desc": "Twenty-third James Bond movie",
"release date": 1350954000000,
"box office": 1110526981
},
{
"id": 4,
"title": "Spectre",
"director": "Sam Mendes",
"starring":
[
"Daniel Craig",
"Lea Seydoux"
],
"desc": "Twenty-fourth James Bond movie",
"release date": 1445821200000,
"box office": 879376275
}
]其中,發佈日期 字段的值是自紀元(Epoch)以來的毫秒數,票房 是電影在美國影院的美元收入。
我們將處理五個不同的 GET 請求工作場景,假設上述 JSON 層次結構已提取並存儲在一個名為 jsonString 的 String 變量中。
6.1. 通過ID獲取對象數據
在用例中,客户端通過向服務器提供電影的精確 id 來請求有關特定電影的詳細信息。此示例演示了服務器如何查找請求的數據,然後再將其返回給客户端。
假設我們需要查找 id 等於 2 的記錄。
第一步是獲取正確的對象數據:
Object dataObject = JsonPath.parse(jsonString).read("$[?(@.id == 2)]");
String dataString = dataObject.toString();JUnit 的 斷言 (Assert) API 確認了多個字段的存在:
assertThat(dataString, containsString("2"));
assertThat(dataString, containsString("Quantum of Solace"));
assertThat(dataString, containsString("Twenty-second James Bond movie"));6.2. 獲取包含主演的電影標題
假設我們想要查找一位名為 Eva Green 的女演員主演的電影。 服務器需要返回包含 Eva Green 在 主演 數組中的 標題。
以下測試將演示如何執行此操作並驗證返回的結果:
@Test
public void givenStarring_whenRequestingMovieTitle_thenSucceed() {
List<Map<String, Object>> dataList = JsonPath.parse(jsonString)
.read("$[?('Eva Green' in @['starring'])]");
String title = (String) dataList.get(0).get("title");
assertEquals("Casino Royale", title);
}6.3. 總收入計算
本場景利用 JsonPath 函數 length() 來確定電影記錄的數量,從而計算所有電影的總收入。
讓我們來看一下實現和測試:
@Test
public void givenCompleteStructure_whenCalculatingTotalRevenue_thenSucceed() {
DocumentContext context = JsonPath.parse(jsonString);
int length = context.read("$.length()");
long revenue = 0;
for (int i = 0; i < length; i++) {
revenue += context.read("$[" + i + "]['box office']", Long.class);
}
assertEquals(594275385L + 591692078L + 1110526981L + 879376275L, revenue);
}6.4. 最高票房電影
本用例演示了使用非默認的 JsonPath 配置選項,即 Option.AS_PATH_LIST,來查找票房最高的電影。
首先,我們需要提取所有電影的票房收入列表,然後將其轉換為數組以便進行排序:
DocumentContext context = JsonPath.parse(jsonString);
List<Object> revenueList = context.read("$[*]['box office']");
Integer[] revenueArray = revenueList.toArray(new Integer[0]);
Arrays.sort(revenueArray);我們可以輕鬆地從排序後的 revenueArray 中提取 highestRevenue 變量,然後利用它來確定具有最高收入的電影記錄的路徑:
int highestRevenue = revenueArray[revenueArray.length - 1];
Configuration pathConfiguration =
Configuration.builder().options(Option.AS_PATH_LIST).build();
List<String> pathList = JsonPath.using(pathConfiguration).parse(jsonString)
.read("$[?(@['box office'] == " + highestRevenue + ")]");根據計算出的路徑,我們將確定並返回對應電影的 標題:
Map<String, String> dataRecord = context.read(pathList.get(0));
String title = dataRecord.get("title");整個過程由 Assert API 驗證:
assertEquals("Skyfall", title);6.5. 一位導演的最新作品
本示例將説明如何確定一位名為 Sam Mendes 的導演的最新作品。
首先,我們創建一個包含所有由 Sam Mendes 導演執導的電影的列表:
DocumentContext context = JsonPath.parse(jsonString);
List<Map<String, Object>> samMendesMovies = context.read("$[?(@.director == 'Sam Mendes')]");我們還可以為根節點 director 定義一個 Filter ,並將其用作謂詞:
Filter directorSamMendesFilter = Filter.filter(Criteria.where("director")
.contains("Sam Mendes"));
List<Map<String, Object>> samMendesMovies = JsonPath.parse(jsonString)
.read("$[?]", directorSamMendesFilter);我們隨後使用該列表來提取發佈日期。這些日期將被存儲在一個數組中,然後進行排序:
List<Object> dateList = new ArrayList<>();
for (Map<String, Object> item : samMendesMovies) {
Object date = item.get("release date");
dateList.add(date);
}
Long[] dateArray = dateList.toArray(new Long[0]);
Arrays.sort(dateArray);我們使用 lastestTime 變量(排序數組的最後一個元素)與 director 字段的值結合,以確定所請求電影的 title:
long latestTime = dateArray[dateArray.length - 1];
List<Map<String, Object>> finalDataList = context.read("$[?(@['director']
== 'Sam Mendes' && @['release date'] == " + latestTime + ")]");
String title = (String) finalDataList.get(0).get("title");以下斷言證明一切都按預期工作:
assertEquals("Spectre", title);7. 結論
本文介紹了 Jayway JsonPath 的基本功能——一個強大的工具,用於遍歷和解析 JSON 文檔。
儘管 JsonPath 存在一些缺點,例如缺乏用於訪問父節點或兄弟節點的操作符,但在許多場景下仍然非常有用。