最近在思考构建一个服务编排(Service Orchestration)系统,考虑这个系统至少需要具备以下特征:
使用统一的方法定义服务功能单元使用一种通用的方式将一个或多个服务的输出映射到下游服务的输入,映射时支持基础的数据转换与处理支持以搭积木的方式将低层服务功能单元组织成更高层抽象的服务功能,直至一个完整的服务用户编排服务时,具备较大的灵活性定制业务
json本质上是一个树形数据结构,同样作为典型树形数据结构的xml文档有XPath(XML Path Language)这种广泛使用的定位和选择节点的表达式语言,JAVA对象也有OGNL(Object-Graph Navigation Language)这种对象属性定位和导航表达式语言,JsonPath类似于XPath,是一种json数据结构节点定位和导航表达式语言。
要使用JsonPath,需要在pom.xml文件中添加相应的依赖:
xml复制代码<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.8.0</version>
</dependency>
为了便于说明,本文用到的json测试数据,如果没有特别说明,使用以下的json数据:
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
}
需要获取指定json指定节点的数据,提供json字符串和JsonPath表达式即可:
java复制代码 // 读取json字符串的expensive叶子节点的值
Object expensive = JsonPath.read(jsonStr, "$.expensive");
log.info("expensive value: {}, class: {}", expensive, expensive.getClass().getCanonicalName());
// 读取json字符串store节点下bicycle节点的值
Object bicycle = JsonPath.read(jsonStr, "$.store.bicycle");
log.info("bicycle value: {}, class: {}", bicycle, bicycle.getClass().getCanonicalName());
// 读取json字符串store下第一个book节点的值
Object book = JsonPath.read(jsonStr, "$.store.book[0]");
log.info("book value: {}, class: {}", book, book.getClass().getCanonicalName());
// 读取json字符串所有的author后代节点的值
Object authors = JsonPath.read(jsonStr, "$..author");
log.info("authors value: {}, class: {}", authors, authors.getClass().getCanonicalName());
// 读取json字符串中所有price值小于10的值
Object books = JsonPath.read(jsonStr, "$.store.book[?(@.price < 10)]");
log.info("books value: {}, class: {}", books, books.getClass().getCanonicalName());
输出如下日志:
text复制代码expensive value: 10, class: java.lang.Integer
bicycle value: {color=red, price=19.95}, class: java.util.LinkedHashMap
book value: {category=reference, author=Nigel Rees, title=Sayings of the Century, price=8.95}, class: java.util.LinkedHashMap
authors value: ["Nigel Rees","Evelyn Waugh","Herman Melville","J. R. R. Tolkien"], class:.NET.minidev.json.JSONArray
books value: [{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95},{"category":"fiction","author":"Herman Melville","title":"Moby Dick","isbn":"0-553-21311-3","price":8.99}], class: net.minidev.json.JSONArray
在JsonPath表达式语法中,$表示json顶层节点,.表示直接孩子,所以$.expensive表示json字符串的直接孩子节点expensive,节点也可以用[]表示。所以$['expensive']与$.expensive等价。如果[]中是数字,则表示数组的某个元素,所以$.store.book[0]表示第1本书。数组支持切片,$.store.book[0:3]表示第1本到第3本书。..则表示任意的后代。*则表示所有的子节点。综合起来如下表所示:
Operator |
Description |
$ |
The root element to query. This starts all path expressions. |
@ |
The current node being processed by a filter predicate. |
* |
Wildcard. AvAIlable anywhere a name or numeric are required. |
.. |
Deep scan. Available anywhere a name is required. |
.<name> |
Dot-notated child |
['<name>' (, '<name>')] |
Bracket-notated child or children |
[<number> (, <number>)] |
Array index or indexes |
[start:end] |
Array slice operator |
[?(<expression>)] |
Filter expression. Expression must evaluate to a boolean value. |
JsonPath还支持根据条件过滤,表达式[?(expression)]中括号内为条件表达式。表达式$.store.book[?(@.price < 10)]中@表示遍历到的当前book,整个表达式就表示store中价格小于10的所有的book。
JsonPath的条件表达式支持以下操作符:
Operator |
Description |
== |
left is equal to right (note that 1 is not equal to '1') |
!= |
left is not equal to right |
< |
left is less than right |
<= |
left is less or equal to right |
left is greater than right |
|
>= |
left is greater than or equal to right |
=~ |
left matches regular expression [?(@.name =~ /foo.*?/i)] |
in |
left exists in right [?(@.size in ['S', 'M'])] |
nin |
left does not exists in right |
subsetof |
left is a subset of right [?(@.sizes subsetof ['S', 'M', 'L'])] |
anyof |
left has an intersection with right [?(@.sizes anyof ['M', 'L'])] |
noneof |
left has no intersection with right [?(@.sizes noneof ['M', 'L'])] |
size |
size of left (array or string) should match right |
empty |
left (array or string) should be empty |
当然一些常用的filter函数也是要支持的:
Function |
Description |
Output type |
min() |
Provides the min value of an array of numbers |
Double |
max() |
Provides the max value of an array of numbers |
Double |
avg() |
Provides the average value of an array of numbers |
Double |
stddev() |
Provides the standard deviation value of an array of numbers |
Double |
length() |
Provides the length of an array |
Integer |
sum() |
Provides the sum value of an array of numbers |
Double |
keys() |
Provides the property keys (An alternative for terminal tilde ~) |
Set<E> |
concat(X) |
Provides a concatinated version of the path output with a new item |
like input |
Append(X) |
add an item to the json path output array |
like input |
first() |
Provides the first item of an array |
Depends on the array |
last() |
Provides the last item of an array |
Depends on the array |
index(X) |
Provides the item of an array of index: X, if the X is negative, take from backwards |
Depends on the array |
Filter Operators |
|
|
详细的JsonPath表达式语法和支持的过滤条件请参考Github官方文档,文档本身介绍的已经很详细了。
从上面的日志结果来看,JsonPath的返回结果的数据类型依情况而定,大概情况是(具体实际情况与底层使用的Json处理组件相关):
JsonPath也支持指定返回值类型,类似于json反序列化
java复制代码Long expensive = JsonPath.parse(jsonStr).read("$.expensive", Long.class);
Bicycle bicycle = JsonPath.parse(jsonStr).read("$.store.bicycle", Bicycle.class);
如果json底层处理组件是jackson或gson,还可以支持泛型,假如使用jackson,先添加jackson-databind依赖:
xml复制代码 <dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
然后配置使用jackson进行json处理和映射即可,如下所示:
java复制代码Configuration.setDefaults(new Configuration.Defaults() {
// 底层使用jackson
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);
}
});
TypeRef<List<String>> typeRef = new TypeRef<List<String>>() {};
List<String> authors = JsonPath.parse(jsonStr).read("$..author", typeRef);
java复制代码Object book = JsonPath.read(jsonStr, "$.store.book[0]");
String target = "{"x": 128}";
String modified = JsonPath.parse(target).set("$.x", book).jsonString();
输出:
text复制代码modified: {"x":{"category":"reference","author":"Nigel Rees","title":"Sayings of the Century","price":8.95}}
可见目标json字符串的节点x的值替换为了book的值,新的值与原先的值可以是完全不同的类型
下面的例子为target json字符串添加了一个名字为k的子节点。注意,这个方法只能作用于json对象,不能是叶子节点或数组
java复制代码Object book = JsonPath.read(jsonStr, "$.store.book[0]");
String target = "{"x": 128}";
String modified = JsonPath.parse(target).put("$", "k", book).jsonString();
java复制代码String target = "{"x": [128]}";
String modified = JsonPath.parse(target).put("$.x", 300).jsonString();
java复制代码String target = "{"x": [128]}";
String modified = JsonPath.parse(target).renameKey("$", "x", "y").jsonString();
java复制代码String target = "{"x": [128]}";
String modified = JsonPath.parse(target).delete("$.x").jsonString();
JsonPath提供了一些配置项来调整它的功能,这些配置项配置在Option枚举类中,如下所示:
java复制代码public enum Option {
DEFAULT_PATH_LEAF_TO_NULL,
ALWAYS_RETURN_LIST,
AS_PATH_LIST,
SUPPRESS_EXCEPTIONS,
REQUIRE_PROPERTIES
}
功能说明:
使用配置:
java复制代码Configuration conf = Configuration.builder()
.options(Option.AS_PATH_LIST).build();
List<String> pathList = JsonPath.using(conf).parse(jsonStr).read("$..author");
// 返回的是author节点的路径列表
assertThat(pathList).containsExactly(
"$['store']['book'][0]['author']",
"$['store']['book'][1]['author']",
"$['store']['book'][2]['author']",
"$['store']['book'][3]['author']");
JsonPath支持多个json组件,默认使用的是JsonSmartJsonProvider,其他的还包括:
复制代码JacksonJsonProvider
JacksonJsonNodeJsonProvider
GsonJsonProvider
JsonOrgJsonProvider
JakartaJsonProvider
使用下面的方式可以修改默认的json provider:
java复制代码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);
}
});
如下的方式每次都需要解析json字符串:
java复制代码Object book = JsonPath.read(jsonStr, "$.store.book[0]");
可以先解析json字符串,然后在查找,这样只需要解析一次:
java复制代码DocumentContext context = JsonPath.parse(jsonStr)
Long expensive = context.read("$.expensive", Long.class);
Bicycle bicycle = context.read("$.store.bicycle", Bicycle.class);
还可以使用cache,JsonPath已经提供了两个cache:
java复制代码com.jayway.jsonpath.spi.cache.LRUCache (default, thread safe)
com.jayway.jsonpath.spi.cache.NOOPCache (no cache)
我们可以这样配置使用cache:
java复制代码CacheProvider.setCache(new LRUCache(100));
或者配置一个我们自己实现的cache:
java复制代码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);
}
})