从 JsonPath 到 SPL
JSON的多层结构可存储丰富的信息,再加上体积小传输效率高,因此被广泛应用于微服务、程序间通讯、配置文件等场景。但多层结构比二维结构格式复杂,计算起来难度很大,这就对JSON类库提出了较高的要求。其中,Gson\Jackson等类库不支持JSON计算语言,只提供了将JSON串解析为JAVA\C#对象的函数。这些类库没有计算能力,即使实现很简单的条件查询,也要编写大量代码,开发效率低且实用性差。
好消息是,JsonPath出现了。
与前面提到的类库不同,JsonPath仿照XPath语法,提供了原始的JSON计算语言,可以用表达式查询出符合条件的节点,并支持一些聚合计算。下面试举几例。
文件data.json存储员工记录以及员工的订单,部分数据如下:
[ {"EId":2,"State":"NewYork ","Dept":"Finance","Name":"Ashley","Gender":"F", "Salary":11000,"Birthday":"1980-07-19", "Orders":[] }, {"EId":3,"State":"New Mexico","Dept":"Sales","Name":"Rachel","Gender":"F", "Salary":9000,"Birthday":"1970-12-17", "Orders":[ {"OrderID":32,"Client":"JFS","SellerId":3,"Amount":468.0,"OrderDate":"2009-08-13"}, … {"OrderID":99,"Client":"RA","SellerId":3,"Amount":1731.2,"OrderDate":"2009-11-05"} ] }, … ]
条件查询:找到员工Rachel的所有订单。JsonPath代码如下:
File file = new File("D:\\json\\data.json"); Long fileLength = file.length(); byte[] fileContent = new byte[fileLength.intValue()]; FileInputStream in = new FileInputStream(file); in.read(fileContent); in.close(); String JsonStr= new String(fileContent, "UTF-8") Object document = Configuration.defaultConfiguration().jsonProvider().parse(JsonStr); ArrayList l=JsonPath.read(document, "$[?(@.Name=='Rachel')].Orders");
上面代码先从文件读入字符串,再转为JSON对象,最后进行查询。具体的查询表达式是 $[?(@.Name=='Rachel')].Orders,其中$代表根节点,即员工记录(含订单字段),Orders代表下层的订单记录(即订单字段),上下层之间用点号隔开。每层节点后面可跟随查询条件,形如[?(…)],具体用法参考官网。
组合查询:找出所有价格在1000-2000,且客户名包含business字样的订单。关键代码如下:
……//省去JSON对象生成过程 l=JsonPath.read(document, "$[*].Orders[?((@.Amount>1000 && @.Amount<2000) && @.Client =~ /.*?business.*?/i )]");
代码中的(@.Amount>1000 && @.Amount<2000)是区间查询条件,@.Client =~ /.*?business.*?/i是正则表达式的模糊查询条件,&&是逻辑运算符“与”(|| 是“或”)。
聚合计算:统计所有订单的总金额。关键代码如下:
…… Double d=JsonPath.read(document, "$.sum($[*].Orders[*].Amount)");
代码中的sum是求和函数,类似的函数还有平均、max、min、计数。
从这些例子可以看出来,JsonPath的语法直观易懂,可以用点号方便地访问多层结构,可以用较短的代码实现条件查询,还能进行简单的聚合计算。从Gson\Jackson到JsonPath实现了计算能力从无到有的突破,这要归功于JSON计算语言。
有不表示强。实际上,JsonPath的JSON计算语言还比较原始,计算能力很弱。JsonPath只支持查询和聚合这两种很简单的计算,不支持其他大多数基础计算,离任意和自由的计算更是遥远。要实现大多数基础计算,JsonPath仍然要硬编码实现。
以分组汇总为例:对所有订单按客户分组,统计各组的订单金额。关键代码如下:
…… ArrayList orders=JsonPath.read(document, "$[*].Orders[*]"); Comparator comparator = new Comparator() { public int compare(HashMap record1, HashMap record2) { if (!record1.get("Client").equals(record2.get("Client"))) { return ((String)record1.get("Client")).compareTo((String)record2.get("Client")); } else { return ((Integer)record1.get("OrderID")).compareTo((Integer)record2.get("OrderID")); } } }; Collections.sort(orders, comparator); ArrayList result=new ArrayList(); HashMap currentGroup=(HashMap)orders.get(0); double sumValue=(double) currentGroup.get("Amount"); for(int i = 1;i < orders.size(); i ++){ HashMap thisRecord=(HashMap)orders.get(i); if(thisRecord.get("Client").equals(currentGroup.get("Client"))){ sumValue=sumValue+(double)thisRecord.get("Amount"); }else{ HashMap newGroup=new HashMap(); newGroup.put(currentGroup.get("Client"),sumValue); result.add(newGroup); currentGroup=thisRecord; sumValue=(double) currentGroup.get("Amount"); } } System.out.println(result);
上述代码先用JsonPath取出订单列表,再将订单列表按Client排序,取出第1条作为当前组的初值,然后依次循环剩余的订单。如果当前订单与当前组相比Client不变,则将当前订单的Amount累加到当前组;如果Client改变,则说明当前组已汇总完成。
JsonPath的计算能力很弱,不支持分组汇总,只能硬编码完成大部分计算,这就要求程序员控制所有细节,代码冗长且容易出错。如果换一个分组字段或汇总字段,则要修改多处代码,如果对多个字段分组或汇总,代码还需大量修改,这就很难写出通用代码。除了分组汇总,JsonPath不支持的基础计算还有:重命名、排序、去重、关联计算、集合计算、笛卡尔积、归并计算、窗口函数、有序计算等。JsonPath也不支持将大计算目标分解为基础计算的机制,比如子查询、多步骤计算等。实际上,对大多数计算来说,JsonPath都要硬编码完成。
除了计算能力之外,Jsonpath还有个问题,就是没有自己的数据源接口,即使很简单的文件JSON,也需要硬编码实现。而JSON一般来自http restful,特殊些的会来自MongoDB或elasticSearch,只有引入第三方类库或硬编码才能从这些接口取数,这导致架构复杂、不稳定因素增大、开发效率降低。
JsonPath的JSON计算能力很弱,本质是因为其计算语言过于原始。要想提高JSON计算能力,必须使用更专业的计算语言。
集算器 SPL是个更好的选择。
集算器 SPL是开源的结构化数据\半结构化数据计算语言,提供了丰富的类库和精炼的语法,可以用简短的代码实现所有的基础计算,可将大计算目标拆分为基础计算,支持多种数据源接口,同时提供JDBC集成接口。
同样的条件查询,SPL代码如下:
A | |
1 | =json(file("d:\\json\\data.json").read()) |
2 | =A1.select(Name=="Rachel").Orders |
A1代码从文件读字符串,并转为序表。序表是通用的结构化\半结构化数据对象,JSON是半结构化数据的一种。A2中函数select查询出符合条件的员工记录,Orders表示记录的订单字段(订单列表),上下层之间用.号隔开。
同样的组合查询,SPL代码如下:
A | |
1 | …. //省去JSON对象/序表生成过程 |
2 | =A1.conj(Orders) |
3 | =A2.select((Amount>1000 && Amount<=2000) && like@c(Client,"*business*")) |
A2合并所有员工的订单,A3进行条件查询,like函数用于模糊查询字符串,@c表示不区分大小写。这里用到了多步骤计算,代码逻辑更清晰,也可将A2A3合并为一句。
同样的聚合计算,SPL代码如下:
A | |
1 | …. |
2 | =A1.conj(Orders).sum(Amount) |
代码中的sum是求和函数,类似的函数还有avg\sum\min\count。
这段代码可在SPL的IDE中调试/执行,也可存为脚本文件(比如getSum.dfx),通过JDBC接口在JAVA中调用,具体代码如下:
package Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class test1 { public static void main(String[] args)throws Exception { Class.forName("com.esproc.jdbc.InternalDriver"); Connection connection =DriverManager.getConnection("jdbc:esproc:local://"); Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery("call getSum()"); printResult(result); if(connection != null) connection.close(); } … }
上面的用法类似存储过程,其实SPL也支持类似SQL的用法,即无须脚本文件,直接将SPL代码嵌入JAVA,代码如下:
… ResultSet result = statement.executeQuery("=json(file(\"D:\\data\\data.json\").read()).conj(Orders).sum(Amount)"); …
SPL提供了丰富的库函数,支持各种基础计算,上面的查询和聚合只是其中一部分,更多基础计算如下:
A | ||
1 | …. | |
2 | =A1.conj(Orders).groups(Client;sum(Amount)) | 分组汇总 |
3 | =A1.groups(State,Gender;avg(Salary),count(1)) | 多字段分组汇总 |
45 | =A1.new(Name,Gender,Dept,Orders.OrderID,Orders.Client,Orders.Client,Orders.SellerId,Orders.Amount,Orders.OrderDate) | 关联 |
6 | =A1.sort(Salary) | 排序 |
7 | =A1.id(State) | 去重 |
SPL计算能力强,经常可以简化多层json的计算,比如:文件JSONstr.json的runners字段是子文档,子文档有3个字段:horseId、ownerColours、trainer,其中trainer含有下级字段trainerId ,ownerColours是逗号分割的数组。部分数据如下:
[ { "race": { "raceId":"1.33.1141109.2", "meetingId":"1.33.1141109" }, ... "numberOfRunners": 2, "runners": [ { "horseId":"1.00387464", "trainer": { "trainerId":"1.00034060" }, "ownerColours":"Maroon,pink,dark blue." }, { "horseId":"1.00373620", "trainer": { "trainerId":"1.00010997" }, "ownerColours":"Black,Maroon,green,pink." } ] }, ... ]
现在要按 trainerId分组,统计每组中 ownerColours的成员个数,可用下面的SPL实现。
A | |
1 | =json(file("/workspace/JSONstr.json").read()) |
2 | =A1(1).runners |
3 | =A2.groups(trainer.trainerId; ownerColours.array().count():times) |
SPL计算能力强,经常可以简化复杂的JSON计算。比如通过http restful可取得按时间排序的每日值班情况,部分数据如下:
[{"Date":"2018-03-01","name":"Emily"}, {"Date":"2018-03-02","name":"Emily"}, {"Date":"2018-03-04","name":"Emily"}, {"Date":"2018-03-04","name":"Johnson"}, {"Date":"2018-04-05","name":"Ashley"}, {"Date":"2018-03-06","name":"Emily"}, {"Date":"2018-03-07","name":"Emily"} ……]
一个人通常会持续值班几个工作日,之后再换人,现在要依次计算出每个人连续的值班情况,结果输出为二维表,部分结果如下:
name | begin | end |
Emily | 2018-03-01 | 2018-03-03 |
Johnson | 2018-03-04 | 2018-03-04 |
Ashley | 2018-03-05 | 2018-03-05 |
Emily | 2018-03-06 | 2018-03-07 |
… | … | … |
要获得上述结果,应先将数据按照name进行有序分组,即如果连续几条记录的name都相同,则这几条记录分为同一组,直到name发生变化。注意这种分组可能会使同一个员工分出多组数据,比如Emily。分组后,应按日期取各组的首尾两条,即所求的开始值班日期和结束值班日期。这里涉及有序分组、分组后计算(即窗口函数)、按位置取值等难度较高的计算,常见的计算语言写起来会很繁琐,改用SPL就简单多了。代码如下:
A | |
1 | =json(httpfile("http://127.0.0.1:6868/api/getDuty").read()) |
2 | =duty.group@o(name) |
3 | =A2.new(name,~.m(1).date:begin,~.m(-1).date:end) |
除了较强的计算能力之外,SPL还提供了丰富的数据源接口,除了上面提到的文件、restful,还支持MongoDB、elsticSearch等,详情参考官网。
从Gson\Jackson到JsonPath,JSON计算语言从无到有,从JsonPath到SPL,JSON计算能力由弱到强,每一次质的飞跃,都驱动着开发效率的大幅提升。