問題引入前端
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 }
其餘
歡迎留言,共同探討!