Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 309|回复: 0

服务器端json数据文件分割合并解决方案

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-4-30 16:03:47 | 显示全部楼层 |阅读模式

    问题引入

         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源码

      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

    其他

         欢迎留言,共同探讨!

     

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-18 22:22 , Processed in 0.077552 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表