服務器端json數據文件分割合併解決方案

問題引入前端

     Json 是什麼就很少說了,本文把Json理解成一種協議。java

     印象之中,Json貌似是前端的專屬,其實否則,服務器端組織數據,依然能夠用Json協議。正則表達式

     好比說,某公司有一套測評題目(基於Json協議),這些題目比較珍貴,不想直接放在js中,因此就將題目文件放在服務器端,而後經過一個接口去請求,多一層控制,就多了一層保護,經過在接口上加權限,可保證數據安全。express

     如此一來,服務器端一定會有一個Json文件(純文本文件),Json文件中包含Json數據。json

     假設Json數據結構以下:安全

 1 {
 2   "name": "題庫",
 3   "items": [{
 4     "name": "測評-1",
 5     "items": [/*...*/]
 6   },{
 7     "name": "測評-2",
 8     "items": [/*...*/]
 9   },{
10     "name": "測評-3",
11     "items": [/*...*/]
12   }/*...*/]
13 }

     暫不討論這樣設計的合理性,假定已是這麼設計了,沒法再作更改。但凡有些規模的項目,需求變更都比較頻繁,項目工期也比較緊張,不得不作出妥協,完美的設計是不存在的。服務器

     隨着時間和規模的增加,測評會愈來愈多,並且每一個測評自己包含的數據也很多,這樣一來,這個Json文件會愈來愈大。數據結構

     衆所周知,IO操做是一個巨大的瓶頸,若是Json文件太大,佔用IO過多,將致使性能嚴重降低。同時Json文件太大,也很差管理,不符合開閉原則。app

     所以,咱們迫切須要對Json文件進行拆分,把數據量大、獨立性強、自成一體的Json數據轉移到主體的外部,單獨放在一個Json文件中,這樣不只縮小了單個文件的體積,也方便管理。ide

     其實這樣作最大的優勢是能夠實現懶加載,或者說是按需加載。

     這樣的情景很常見,好比在進行數據檢索時,通常狀況下,會先看到一個數據概要列表,列出幾項重要信息,其餘次要信息須要點擊「詳情」按鈕時,纔去加載。

     拿上邊測評的例子來講,第一步僅需顯示出有哪些測評,而後根據用戶的選擇,再去加載對應測評的詳細信息。沒有必要一上來就把全部的信息都返回給客戶端,不只浪費資源,還下降了數據安全性。

     如何才能實現Json文件的合併呢?請看下章~~~

解決方案:Jean

     Jean是一個Java工具類,她能夠實現Json文件合併、依賴管理,靈感來自於前端模塊化開發。

     這名字是怎麼來的呢?前端模塊化開發,國內比較厲害的就是Sea.js了,小菜要寫的是Java工具類,要不就叫Jea?因而趕忙上網查查Jea有沒有啥特殊含義,萬一是敏感詞就很差了。結果一查,查到了Jean,可翻譯爲「珍」,至關不錯的名字嘛,就是她了!

     Jean的思想是在Json文件中,加入一段特殊代碼,來引入其餘Json文件,有點像Jsp中的include。語法爲:@Jean("family","./items/family.js")。能夠把@Jean()理解成函數調用,裏邊有兩個參數,第一個參數是屬性名稱,第二個參數是依賴文件的相對路徑。

     文章開篇測評的例子,能夠寫成這樣:

 1 {
 2   "name": "題庫",
 3   "items": [{
 4     "name": "測評-1",
 5     @Jean("items","./items1/test.js")
 6   },{
 7     "name": "測評-2",
 8     @Jean("items","./items2/test.js")
 9   },{
10     @Jean("items","./items3/test.js"),
11     "name": "測評-3"
12   }/*...*/]
13 }

     假設./items1/test.js中內容爲:

1 {
2   name: "測評-1-內容"
3 }

     由此能夠看出,@Jean在Json文件中的寫法,就和普通的屬性寫法同樣,若是是寫在最後邊,末尾就不用加逗號,其餘狀況一樣須要加逗號。

     經過工具類解析以後,@Jean("items","./items1/test.js")會變成:"items": {name: "測評-1-內容"},替換以後,爲了保證格式正確,因此寫@Jean的時候須要按照正常的語法加逗號。

     第一個參數,將會轉換成@Jean佔位符被替換後的Json屬性名稱,若是不寫,默認爲"jean"。

第二個參數是該屬性依賴的Json文件的相對路徑,固然是相對於當前Json文件的,Jean會根據當前Json文件的路徑,找到依賴的Json文件,而後讀取內容,再合併到當前Json文件中。目前小菜實現的Jean工具類,只能識別./和../兩種相對路徑語法(含義與HTML相對路徑語法相同)。

     因此,@Jean僅僅是一個佔位符,包含有@Jean的Json字符串,必須通過Jean工具類處理以後,纔是合法的Json字符串。同時,Jean僅僅關心依賴,而不關心依賴的組織形式,這樣能夠帶來巨大的靈活性,不管怎樣組織文件結構,最終體現到Jean的僅僅是一個相對路徑而已。

     Jean工具類提供了三個public方法:

 1 /**
 2  * 解析全部的jean表達式
 3  * @param json json字符串
 4  * @param jsonPath json字符串所在路徑,完整路徑
 5  * @return 解析後的json字符串
 6  */
 7 public static String parseAll(String json,String jsonPath){}
 8 
 9 /**
10  * 解析單個jean表達式
11  * @param express jean表達式
12  * @param jsonPath json字符串所在路徑,完整路徑
13  * @return 解析結果
14  */
15 public static String parseOne(String express,String jsonPath){}
16 
17 /**
18  * 解析特定的jean表達式
19  * @param json json字符串
20  * @param jsonPath json字符串所在路徑,完整路徑
21  * @param names 須要解析的屬性名稱列表
22  * @return 解析後的json字符串
23  */
24 public static String parseTarget(String json,String jsonPath,List<String> names){}

     第一個方法就是說給我一個包含@Jean的Json字符串,再給我這個Json字符串所在文件的絕對路徑,我就把全部的@Jean解析成依賴文件中的內容。

     爲啥非要單獨傳入一個絕對路徑呢?其實能夠直接傳入Json文件的路徑,這樣既能拿到須要解析的Json字符串,又能獲取當前Json文件的絕對路徑。但這樣有一個缺點,就是每調用一次,就要讀一次文件,小菜單獨把路徑寫成一個參數,就是要把讀文件的過程留給用戶,具體怎麼讀,由用戶說了算,最終把須要解析的Json字符串和參照路徑給我就能夠了。例如:

1 String json = "{@Jean(\"item1\",\"./../../item.js\"),@Jean(\"item2\",\"../item.js\")}";
2 System.out.println(parseAll(json, "E:/root/json")); //print {"item1": {"name": "xxx1"},"item2": {"name": "xxx2"}}

     第二個方法能夠直接解析一個@Jean表達式,很少解釋。例如:

1 String expression = "@Jean(\"item1\",\"./../../item.js\")";
2 System.out.println(parseOne(expression, "E:/root/json")); //print "item1": {"name": "xxx1"}

     第三個方法能夠解析指定的@Jean表達式,@Jean表達式第一個參數是屬性名稱,想解析哪一個屬性,就把它放在List<String>中,其餘不作解析的,屬性值爲null。這樣就實現了懶加載。例如:

1 List<String>  names = new ArrayList<String>();
2 names.add("item1");
3 String json = "{@Jean(\"item1\",\"./../../item.js\"),@Jean(\"item2\",\"../item.js\")}";
4 System.out.println(parseTarget(json, "E:/root/json", names)); //print {"item1": {"name": "xxx"},"item2": null}

Jean源碼

  1 import java.io.BufferedReader;
  2 import java.io.File;
  3 import java.io.FileInputStream;
  4 import java.io.IOException;
  5 import java.io.InputStreamReader;
  6 import java.util.ArrayList;
  7 import java.util.HashMap;
  8 import java.util.List;
  9 import java.util.Map;
 10 import java.util.regex.Matcher;
 11 import java.util.regex.Pattern;
 12 
 13 
 14 /**
 15  * json文件合併工具類
 16  * @author 楊元
 17  */
 18 public class Jean {
 19     
 20     /**
 21      * 識別jean表達式
 22      */
 23     private static Pattern jeanRegex = Pattern.compile("(@Jean\\((\"[^\"]*\",)?\"[^\"]*\"\\))");
 24     /**
 25      * 識別jean表達式中的全部參數
 26      */
 27     private static Pattern paramRegex = Pattern.compile("\"([^\"]*)\"");
 28     /**
 29      * 識別jean表達式中的name參數
 30      */
 31     private static Pattern nameRegex = Pattern.compile("\"([^\"]*)\",");
 32     /**
 33      * 默認屬性名稱
 34      */
 35     private static String defaultName = "jean";
 36     
 37     /**
 38      * 解析全部的jean表達式
 39      * @param json json字符串
 40      * @param jsonPath json字符串所在路徑,完整路徑
 41      * @return 解析後的json字符串
 42      */
 43     public static String parseAll(String json,String jsonPath){
 44         //識別jean表達式
 45         List<String> jeans = regexMatchList(jeanRegex, json);
 46         jeans = noRepeat(jeans);
 47         
 48         //解析
 49         for(String jean : jeans){
 50             json = json.replace(jean, parse(jean, jsonPath));
 51         }
 52         
 53         return json;
 54     }
 55     
 56     /**
 57      * 解析單個jean表達式
 58      * @param express jean表達式
 59      * @param jsonPath json字符串所在路徑,完整路徑
 60      * @return 解析結果
 61      */
 62     public static String parseOne(String express,String jsonPath){
 63         return parse(express, jsonPath);
 64     }
 65     
 66     /**
 67      * 解析特定的jean表達式
 68      * @param json json字符串
 69      * @param jsonPath json字符串所在路徑,完整路徑
 70      * @param names 須要解析的屬性名稱列表
 71      * @return 解析後的json字符串
 72      */
 73     public static String parseTarget(String json,String jsonPath,List<String> names){
 74         //識別jean表達式
 75         List<String> jeans = regexMatchList(jeanRegex, json);
 76         jeans = noRepeat(jeans);
 77         //處理屬性名映射
 78         Map<String, Boolean> nameMap = new HashMap<String, Boolean>();
 79         for(String s : names){
 80             nameMap.put(s, true);
 81         }
 82         
 83         //解析
 84         String replacement = "";
 85         Matcher matcher = null;
 86         String name = "";
 87         for(String jean : jeans){
 88             matcher = nameRegex.matcher(jean);
 89             
 90             //判斷是否傳入屬性名稱
 91             if(matcher.find()){
 92                 name = matcher.group(1);
 93                 //判斷是否須要解析
 94                 if(nameMap.get(name) != null){
 95                     replacement = parse(jean, jsonPath);
 96                 }else{
 97                     //不須要解析直接將屬性值寫爲null
 98                     replacement = "\""+name+"\": null";
 99                 }
100             }else{
101                 //無屬性名直接用默認的jean
102                 replacement = "\""+defaultName+"\": null";
103             }
104             
105             json = json.replace(jean, replacement);
106         }
107         
108         return json;
109     }
110     
111     /**
112      * 解析jean表達式
113      * @param express jean表達式
114      * @param jsonPath json文件所在路徑,完整路徑
115      * @return jean表達式執行結果
116      */
117     private static String parse(String express,String jsonPath){
118         //識別參數
119         List<String> params = regexMatchList(paramRegex, express);
120         //默認屬性名稱
121         String name = defaultName;
122         //格式化路徑
123         jsonPath = removeSuffix(jsonPath, "/");
124         
125         //判斷是否傳入了屬性名稱
126         if(params.size() > 1){
127             name = params.get(0);
128         }
129         
130         //解析路徑
131         String path = getAbsolutePath(jsonPath, params.get(params.size()-1));
132         
133         //讀取內容並返回
134         name = wrapWith(name, "\"");
135         return name + ": " + readJsonFile(path);
136     }
137     
138     /**
139      * 從字符串中移除指定後綴
140      * @param source 源字符串
141      * @param suffix 須要移除的後綴
142      * @return 處理後的源字符串
143      */
144     private static String removeSuffix(String source,String suffix){
145         if(source.endsWith(suffix)){
146             source = source.substring(0, source.length()-suffix.length());
147         }
148         
149         return source;
150     }
151     
152     /**
153      * list內容去重
154      * @param list 內容爲string的list
155      * @return 內容去重後的list
156      */
157     private static List<String> noRepeat(List<String> list){
158         Map<String, String> map = new HashMap<String, String>();
159         List<String> result = new ArrayList<String>();
160         
161         for(String s : list){
162             map.put(s, null);
163         }
164         
165         for(String s : map.keySet()){
166             result.add(s);
167         }
168         
169         return result;
170     }
171     
172     /**
173      * 用指定的字符串包裹內容
174      * @param content 內容
175      * @param wrap 包裹字符串
176      * @return 包裹後的內容
177      */
178     private static String wrapWith(String content,String wrap){
179         return wrap+content+wrap;
180     }
181     
182     /**
183      * 讀取Json文件(純文本文件,utf-8編碼)
184      * 這個方法能夠替換成本身項目中封裝的方法
185      * @param path 文件路徑
186      * @return 文件內容
187      */
188     private static String readJsonFile(String path){
189         String encoding = "utf-8";
190         StringBuilder sb = new StringBuilder(256);
191         
192         File file = new File(path);
193         InputStreamReader iReader = null;
194         BufferedReader bReader = null;
195         
196         try{
197             iReader = new InputStreamReader(new FileInputStream(file), encoding);
198             bReader = new BufferedReader(iReader);
199             String line = null;
200             
201             while((line = bReader.readLine()) != null){
202                 sb.append(line.trim());
203             }
204             
205             bReader.close();
206             iReader.close();
207             
208         }catch(Exception e){
209             if(iReader != null){
210                 try {
211                     iReader.close();
212                 } catch (IOException e1) {
213                     iReader = null;
214                 }
215             }
216             if(bReader != null){
217                 try {
218                     bReader.close();
219                 } catch (IOException e1) {
220                     bReader = null;
221                 }
222             }
223         }
224         
225         return sb.toString();
226     }
227     
228     /**
229      * 將相對路徑轉換成絕對路徑
230      * 只識別 ./ ../
231      * @param refrence 基準參照路徑
232      * @param relative 相對路徑表達式
233      * @return 絕對路徑
234      */
235     private static String getAbsolutePath(String refrence,String relative){
236         if(relative.startsWith("./")){
237             refrence = getAbsolutePath(refrence, relative.replaceFirst("\\./", ""));
238         }else if(relative.startsWith("../")){
239             refrence = getAbsolutePath(refrence.substring(0, refrence.lastIndexOf("/")), 
240                             relative.replaceFirst("\\.\\./", ""));
241         }else{
242             refrence = refrence + "/" + relative;
243         }
244         
245         return refrence;
246     }
247     
248     /**
249      * 將正則表達式的匹配結果轉換成列表
250      * @param regex 正則表達式對象
251      * @param input 要檢索的字符串
252      * @return 結果列表
253      */
254     private static List<String> regexMatchList(Pattern regex,String input){
255         List<String> result = new ArrayList<String>();
256         Matcher matcher = regex.matcher(input);
257         while(matcher.find()){
258             result.add(matcher.group(1));
259         }
260         
261         return result;
262     }
263 
264 
265 }
View Code

其餘

     歡迎留言,共同探討!

相關文章
相關標籤/搜索