服务器端json数据文件分割合并解决方案
问题引入
Json 是什么就不多说了,本文把Json理解成一种协议。
印象之中,Json貌似是前端的专属,其实不然,服务器端组织数据,依然可以用Json协议。
比如说,某公司有一套测评题目(基于Json协议),这些题目比较珍贵,不想直接放在js中,所以就将题目文件放在服务器端,然后通过一个接口去请求,多一层控制,就多了一层保护,通过在接口上加权限,可保证数据安全。
如此一来,服务器端必定会有一个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文件太大,也不好管理,不符合开闭原则。
因此,我们迫切需要对Json文件进行拆分,把数据量大、独立性强、自成一体的Json数据转移到主体的外部,单独放在一个Json文件中,这样不仅缩小了单个文件的体积,也方便管理。
其实这样做最大的优点是可以实现懒加载,或者说是按需加载。
这样的情景很常见,比如在进行数据检索时,一般情况下,会先看到一个数据概要列表,列出几项重要信息,其他次要信息需要点击“详情”按钮时,才去加载。
拿上边测评的例子来说,第一步仅需显示出有哪些测评,然后根据用户的选择,再去加载对应测评的详细信息。没有必要一上来就把所有的信息都返回给客户端,不仅浪费资源,还降低了数据安全性。
如何才能实现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源码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 }
其他
欢迎留言,共同探讨!