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字樣的訂單。關鍵代碼如下:
……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)");從這些例子可以看出來,JsonPath的語法直觀易懂,可以用點號方便地訪問多層結構,可以用較短的代碼實現條件查詢,還能進行簡單的聚合計算。從Gson\Jackson到JsonPath實現了計算能力從無到有的突破,這要歸功於JSON計算語言。代碼中的sum是求和函數,類似的函數還有平均、max、min、計數。有不表示強。實際上,JsonPath的JSON計算語言還比較原始,計算能力很弱。JsonPath只支持查詢和聚合這兩種很簡單的計算,不支持其他大多數基礎計算,離任意和自由的計算更是遙遠。要實現大多數基礎計算,JsonPath仍然要硬編碼實現。
以分組匯總為例:對所有訂單按客戶分組,統計各組的訂單金額。關鍵代碼如下:
……ArrayList orders=JsonPath.read(document, "$[*].Orders[*]");Comparator<HashMap> comparator = new Comparator<HashMap>() { 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<HashMap> result=new ArrayList<HashMap>();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代碼如下:
A1=json(file("d:\\json\\data.json").read())2=A1.select(Name=="Rachel").OrdersA1代碼從文件讀字符串,並轉為序表。序表是通用的結構化\半結構化數據對象,JSON是半結構化數據的一種。A2中函數select查詢出符合條件的員工記錄,Orders表示記錄的訂單欄位(訂單列表),上下層之間用.號隔開。
A1…. //省去JSON對象/序表生成過程2=A1.conj(Orders)3=A2.select((Amount>1000 && Amount<=2000) && like@c(Client,"*business*"))A2合併所有員工的訂單,A3進行條件查詢,like函數用於模糊查詢字符串,@c表示不區分大小寫。這裡用到了多步驟計算,代碼邏輯更清晰,也可將A2A3合併為一句。
A1….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))多欄位分組匯總4=A1.new(Name,Gender,Dept,Orders.OrderID,Orders.Client,Orders.Client,Orders.SellerId,Orders.Amount,Orders.OrderDate)關聯5=A1.sort(Salary)排序6=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實現。
A1=json(file("/workspace/JSONstr.json").read())2=A1(1).runners3=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"}……]一個人通常會持續值班幾個工作日,之後再換人,現在要依次計算出每個人連續的值班情況,結果輸出為二維表,部分結果如下:
namebeginendEmily2018-03-012018-03-03Johnson2018-03-042018-03-04Ashley2018-03-052018-03-05Emily2018-03-062018-03-07………要獲得上述結果,應先將數據按照name進行有序分組,即如果連續幾條記錄的name都相同,則這幾條記錄分為同一組,直到name發生變化。注意這種分組可能會使同一個員工分出多組數據,比如Emily。分組後,應按日期取各組的首尾兩條,即所求的開始值班日期和結束值班日期。這裡涉及有序分組、分組後計算(即窗口函數)、按位置取值等難度較高的計算,常見的計算語言寫起來會很繁瑣,改用SPL就簡單多了。代碼如下:
A1=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計算能力由弱到強,每一次質的飛躍,都驅動著開發效率的大幅提升。簡單好用的SPL開源啦!
為了給感興趣的小夥伴們提供一個相互交流的平臺,
特地開通了交流群(群完全免費,不廣告不賣課)