JsonPath:從多層嵌套Json中解析所需要的值
問題
應用中,常常要從嵌套的JSON串中解析出所需要的數據。通常的做法是,先將JSON轉換成Map, 然後一層層地判空和解析。可使用 JsonPath 來解決這個問題。
給定一個 JSON 串如下所示
{"code":200,"msg":"ok","list":[{"id":20,"no":"1000020","items":[{"name":"n1","price":21,"infos":{"feature":""}}]}],"metainfo":{"total":20,"info":{"owner":"qinshu","parts":[{"count":13,"time":{"start":1230002456,"end":234001234}}]}}}
從中解析出 code, total, count 的值。
基本方案
基本方案就是自己手動將JSON轉爲Map,然後一層層判空和解析,如下代碼所示:
public class JsonUtil {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
// 爲保持對象版本兼容性,忽略未知的屬性
MAPPER.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 序列化的時候,跳過null值
MAPPER.getSerializationConfig().setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
// date類型轉化
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
MAPPER.setDateFormat(fmt);
}
/**
* 將一個json字符串解碼爲java對象
*
* 注意:如果傳入的字符串爲null,那麼返回的對象也爲null
*
* @param json json字符串
* @param cls 對象類型
* @return 解析後的java對象
* @throws RuntimeException 若解析json過程中發生了異常
*/
public static <T> T toObject(String json, Class<T> cls) {
if (json == null) {
return null;
}
try {
return MAPPER.readValue(json, cls);
} catch (Exception e) {
return null;
}
}
/**
* 讀取JSON字符串爲MAP
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> readMap(String json) {
return toObject(json, HashMap.class);
}
/**
* 對於正確JSON及存在的Path下獲取到最終指定值並轉成字符串,其他情況一律返回 null
* @param json JSON串
* @param path 點分隔的字段路徑
* @return 相應字段的字符串值
*/
public static String readVal(String json, String path) {
if (json == null || path == null) {
return null;
}
Map<String,Object> map = readMap(json);
if (map == null) {
// log.warn("parse json failed: " + json);
return null;
}
String[] subpaths = path.split("\\.");
return readVal(map, subpaths);
}
private static String readVal(Map<String, Object> map, String path) {
return readVal(map, path.split("\\."));
}
private static String readVal(Map<String, Object> map, String[] subpaths) {
Object val = map;
try {
for (String subpath: subpaths) {
if (val != null && val instanceof Map) {
val = ((Map)val).get(subpath);
}
else {
// log.warn("subpath may not exists in " + map);
return null;
}
}
return val == null ? null: val.toString();
} catch (Exception ex) {
return null;
}
}
realVal 的目標就是:對於正常情況下獲取到最終指定值並轉成字符串,其他情況一律返回 null. readVal 上層函數接受一個JSON串和一個點分割的Path,進行參數校驗後交給下層 readVal 函數;下層 readVal 函數對每次取出的值進行判空和取值,如果OK就一直進行到取出最終值;否則要麼拋出異常,要麼直接返回 null.
對於只需要從嵌套Map中取值的需求,基本是滿足了,不過若要從List,Map混合的JSON串中取值,就不夠用了。此時,可採用成熟庫 JsonPath 來完成這件事。
JsonPath
在網上搜索 jsonpath maven 即可在 mvnrepository 找到 jsonpath 的最新版本。工程中引入
<!-- https://mvnrepository.com/artifact/com.jayway.jsonpath/json-path -->
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
</dependency>
現在就可以使用JsonPath 了。 基本語法可參考這篇文章: https://www.cnblogs.com/weilunhui/p/3857366.html
可以爲 JsonPath 包裝一層函數, 與現有客戶端使用相同。
public static String readValUsingJsonPath(String json, String path) {
if (json == null || path == null) {
return null;
}
try {
Object val = JsonPath.read(json, "$." + path);
return val == null ? null : val.toString();
} catch (Exception ex) {
return null;
}
}
編寫單測如下:
public class JsonUtilTest extends CommonForTest {
String json = "{\"code\":200,\"msg\":\"ok\",\"list\":[{\"id\":20,\"no\":\"1000020\",\"items\":[{\"name\":\"n1\",\"price\":21,\"infos\":{\"feature\":\"\"}}]}],\"metainfo\":{\"total\":20,\"info\":{\"owner\":\"qinshu\",\"parts\":[{\"count\":13,\"time\":{\"start\":1230002456,\"end\":234001234}}]}}}";
// 常用的 Json Path 可以緩存起來重用,類似正則裏的 Pattern p = Pattern.compile('regexString')
JsonPath codePath = JsonPath.compile("$.code");
JsonPath totalPath = JsonPath.compile("$.metainfo.total");
@Test
public void testReadVal() {
eq(null, JsonUtil.readVal(null, "code"));
eq(null, JsonUtil.readVal(json, null));
eq("200", JsonUtil.readVal(json, "code"));
eq("20", JsonUtil.readVal(json, "metainfo.total"));
eq("qinshu", JsonUtil.readVal(json, "metainfo.info.owner"));
eq(null, JsonUtil.readVal("invalid json", "code"));
eq(null,JsonUtil.readVal(json, "metainfo.extra.feature"));
eq(null, JsonUtil.readValUsingJsonPath(null, "code"));
eq(null, JsonUtil.readValUsingJsonPath(json, null));
eq("200", JsonUtil.readValUsingJsonPath(json, "code"));
eq("20", JsonUtil.readValUsingJsonPath(json, "metainfo.total"));
eq("qinshu", JsonUtil.readValUsingJsonPath(json, "metainfo.info.owner"));
eq(null, JsonUtil.readValUsingJsonPath("invalid json", "code"));
eq(null,JsonUtil.readValUsingJsonPath(json, "metainfo.extra.feature"));
eq(200, codePath.read(json));
eq(20, totalPath.read(json));
eq("qinshu", JsonPath.read(json, "$.metainfo.info.owner"));
eq("n1", JsonPath.read(json, "$.list[0].items[0].name"));
eq(13, JsonPath.read(json, "$.metainfo.info.parts[0].count"));
}
}
可見 jsonPath 的功能更加強大,也更加健壯。
小結
多熟悉現有庫,不輕易造重複輪子。
作者:@琴水玉