2020寒假作业(2/2)

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/2020SpringW
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/2020SpringW/homework/10281
这个作业的目标 考察需求分析,学习git、github
作业正文 https://www.cnblogs.com/herokilito/p/12264891.html
其他参考文献 ...

1、GitHub

  • GitHub用户名

    herokilito

  • 本次作业仓库地址

    https://github.com/herokilito/InfectStatistic-main

  • 第一次作业相关仓库

    • Spring
      简介:MyBatis-Spting适配器,会帮助你将 MyBatis 代码无缝地整合到 Spring 中。
    • Spring-framework
      简介:Spring框架,Spring提供了Java编程语言以外的所有所需内容,可用于为各种场景和体系结构创建企业应用程序。
    • mybatis-3
      简介:对象关系映射工具,简单性是MyBatis数据映射器的最大优势。
    • Spring-boot
      简介:Spring Boot使创建具有Spring动力的生产级应用程序和服务变得非常容易。
    • SpringCloudLearning
      简介:SpringCloud教程

2、PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
Estimate 估计这个任务需要多少时间 1200 1330
Development 开发
Analysis 需求分析 (包括学习新技术) 300 200
Design Spec 生成设计文档 60 80
Design Review 设计复审 10 20
Coding Standard 代码规范 (为目前的开发制定合适的规范) 30 20
Design 具体设计 150 100
Coding 具体编码 420 540
Code Review 代码复审 30 60
Test 测试(自我测试,修改代码,提交修改) 120 200
Reporting 报告
Test Report 测试报告 20 20
Size Measurement 计算工作量 10 10
Postmortem & Process Improvement Plan 事后总结, 并提出过程改进计划 30 20
合计 1200

3、分析与设计

  • 思路描述

    • 首先分离参数,判断传入了什么参数,记录各个参数的参数值。
    • 每种参数都设计一个方法来实现相应的功能。
    • 根据记录的参数按特定的顺序一个个执行相应的方法。
    • 在执行方法的过程发生未预期的错误直接结束进程并显示错误信息。
    • 数据可以用键值对的方式保存,如java中的map。
  • 程序设计

    • 代码组织
      • 方法划分
        有五种参数,根据参数类型划分成五个方法
        -log参数:dolog()
        -out参数:doOut()
        -date参数:doDate()
        -type参数:doType()
        -province参数:doProvince()
        日志中可能出现八种情况,根据不同情况划分不同的计算方法
        <省> 新增 感染患者 n人:increaseInf()
        <省> 新增 疑似患者 n人:increaseSus()
        <省1> 感染患者 流入 <省2> n人:infInflow()
        <省1> 疑似患者 流入 <省2> n人:susInflow()
        <省> 死亡 n人:dead()
        <省> 治愈 n人:cure()
        <省> 疑似患者 确诊感染 n人:diagnose()
        <省> 排除 疑似患者 n人:exclude()
      • 数据结构
        观察预期输出数据可知,使用某种数据结构即能保存省份名,又能保存省份数据便于简化编程,于是使用了java的Map接口。
        因为输出数据需要按拼音数据排序,于是选择了LinkedHashMap实例。
        LinkedHashMap<String,List> String为省份名,List为数据列表保存该省份四种数据,每个数据项用整型存储。
    • 主要函数流程图

      流程图

4、主要代码

public void execute(String[] args) throws Lib.Exit {
 if(args.length == 1){
     Lib.helpList();    //显示提示信息
     throw new Lib.Exit("请按照提示输入命令");
 }
 /*分离参数*/
 int i = 1;
 while (i < args.length) {
     switch (args[i]) {
         case "-log":
             hasLog = true;
             if (++i >= args.length) {  //如果-log后面没有给参数值
                 throw new Lib.Exit("-log参数缺少参数值");
             }
             logParam = args[i++];      //-log后面跟着的参数为-log的参数值
             break;
         case "-out":
             hasOut = true;
             if (++i >= args.length) {  //如果-out后面没有给参数值
                 throw new Lib.Exit("-out参数缺少参数值");
             }
             outParam = args[i++];      //-out后面跟着的参数为-out的参数值
             break;
         case "-date":
             hasDate = true;
             if (++i >= args.length) {  //如果-date后面没有给参数值
                 throw new Lib.Exit("-date参数缺少参数值");
             }
             dateParam = args[i++];     //-date后面跟着的参数为-date的参数值
             break;
         case "-type":
             hasType = true;
             while (++i < args.length && !args[i].equals("-log") && !args[i].equals("-out") && !args[i].equals("-date")
                     && !args[i].equals("-province")) {   //-type的参数值范围
                 typeParams.add(args[i]);
             }
             break;
         case "-province":
             hasProvince = true;
             while (++i < args.length && !args[i].equals("-log") && !args[i].equals("-out") && !args[i].equals("-date")
                     && !args[i].equals("-type")) {       //-province的参数值范围
                 provinceParams.add(args[i]);
             }
             break;
         default:
             throw new Lib.Exit("\"" + args[i] + "\"无法解析的参数");
     }
 }
 /*执行相应的方法*/
 if(!hasLog){  //log必须有
     throw new Lib.Exit("缺少-log参数");
 }
 if(!hasOut){  //out必须有
     throw new Lib.Exit("缺少-out参数");
 }
 if(!hasDate){  //如果没有data参数
     dateParam=new SimpleDateFormat("yyyy-MM-dd").format(new Date()); //当前日期
 }
 doLog(logParam);    //读取日志路径
 doDate(dateParam);   //读取日志路径下相应日期的日志
 if(hasType){
     doType(typeParams);   //需要输出的信息类型
 }
 if(hasProvince){
     doProvince(provinceParams);   //需要输出的省份疫情信息
 }
 doOut(outParam);  //输出到指定的路径
}

说明:判断传入的参数、参数值,调用相应的方法,具体请看程序注释。

private void doDate(String date) throws Lib.Exit {
 List<File> logList = Lib.getLogFiles(logDirectory);
 DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
 Date paramDate;
 BufferedReader reader = null;
 try {
     paramDate = dateFormat.parse(date);
     List<Integer> nationalData = statistics.get("全国"); //全国数据
     for (File log : logList) {
         Date logDate = dateFormat.parse(log.getName().substring(0, log.getName().indexOf('.')));
         if(logDate.compareTo(paramDate) > 0) {  //判断日志文件的日期是否小于等于给定日期
             continue;
         }
         reader = new BufferedReader(new InputStreamReader(new FileInputStream(log), StandardCharsets.UTF_8));
         String dataRow;
         while((dataRow = reader.readLine()) != null){
             if(dataRow.startsWith("//")) { //忽略注释行
                 continue;
             }
             String[] data = dataRow.split(" ");  //分割数据行
             if(!outProvince.contains(data[0])){
                 outProvince.add(data[0]);
             }
             List<Integer> provinceData = statistics.get(data[0]);   //当前行的省份数据
             List<Integer> destProvince;   //用于处理流入
             switch (data[1]) {
                 case INCREMENT:  //处理新增
                     if (data[2].equals(INFECTION_PATIENT)) {  //新增感染
                         increaseInf(nationalData, provinceData, Lib.parseData(data[3]));
                     } else {                                  //新增疑似
                         increaseSus(nationalData, provinceData, Lib.parseData(data[3]));
                     }
                     break;
                 case EXCLUDE:  //处理排除疑似
                     excludeSus(nationalData, provinceData, Lib.parseData(data[3]));
                     break;
                 case CURE:  //处理治愈
                     cure(nationalData,provinceData,Lib.parseData(data[2]));
                     break;
                 case DEAD:  //处理死亡
                     dead(nationalData,provinceData,Lib.parseData(data[2]));
                     break;
                 case INFECTION_PATIENT:  //处理感染患者流入
                     destProvince = statistics.get(data[3]);
                     infInflow(provinceData,destProvince,Lib.parseData(data[4]));
                     break;
                 case SUSPECTED_PATIENT:
                     if(data[2].equals(INFLOW)){   //处理疑似患者流入
                         destProvince = statistics.get(data[3]);
                         susInflow(provinceData,destProvince,Lib.parseData(data[4]));
                     } else if(data[2].equals(DIAGNOSE)) {  //处理确诊
                         diagnose(nationalData,provinceData,Lib.parseData(data[3]));
                     }
                     break;
             }
         }
     }
 }catch (Exception e){
     throw new Lib.Exit(e.getMessage());
 }finally {
     try{
         if (reader != null) {
             reader.close();
         }
     }catch (Exception e){
         e.printStackTrace();
     }
 }
}

说明:读取日志信息,判断日志内容属于哪一类,并调用相应的计算方法,具体请看程序注释。

5、单元测试,单元测试覆盖率

  • 单元测试说明

    单元测试所用工具为IDEA的Junit5插件,测试所用日志数据为作业模板给的三个日志文件

  • 单元测试展示及测试结果

@Test
void testLogOut1() {   
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut1.txt" ,
 };
 InfectStatistic.main(args);
}

listOut1
说明:基本参数的测试,只有-log和-out参数

@Test
void testLogOut2() {
 String[] args = {
         "list" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut2.txt" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
 };
 InfectStatistic.main(args);
}

listOut2
说明:基本参数的测试,测试参数顺序是否会影响结果

@Test
void testDate1() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut3.txt" ,
         "-date" , "2020-01-22"
 };
 InfectStatistic.main(args);
}

listOut3
说明:-date参数的测试,测试日期在日志文件之内

@Test
void testDate2() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut4.txt" ,
         "-date" , "2020-2-1"
 };
 InfectStatistic.main(args);
}

listOut4
说明:-date参数的测试,测试日期在日志文件之外

@Test
void testType1() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut5.txt" ,
         "-type" , "ip" , "sp" , "cure" , "dead" ,
 };
 InfectStatistic.main(args);
}

listOut5
说明:测试-type参数,包含全部-type参数值

@Test
void testType2() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut6.txt" ,
         "-type" , "cure" , "dead" , "ip" ,
 };
 InfectStatistic.main(args);
}

listOut6
说明:测试-type参数,包含部分-type参数值且参数值顺序改变

@Test
void testProvince1() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut7.txt" ,
         "-province" , "全国" , "福建" , "湖北" ,
 };
 InfectStatistic.main(args);
}

listOut7
说明:测试-province参数

@Test
void testProvince2() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut8.txt" ,
         "-province" , "全国" , "浙江" , "福建" ,
 };
 InfectStatistic.main(args);
}

listOut8
说明:测试-province参数

@Test
void testAll1() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut9.txt" ,
         "-date" , "2020-01-27" ,
         "-type" , "ip" , "sp" , "dead" , "cure" ,
         "-province" , "福建" , "浙江" , "河北" , "湖北"
 };
 InfectStatistic.main(args);
}

listOut9
说明:测试全部参数

@Test
void testAll2() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut10.txt" ,
         "-date" , "2020-01-23" ,
         "-type" , "ip" , "dead" ,"cure" ,
         "-province" , "福建" , "浙江" , "河北" , "湖北"
 };
 InfectStatistic.main(args);
}

listOut10
说明:测试全部参数

@Test
void testHelp() {
 String[] args = {
 };
 InfectStatistic.main(args);
}

listOut11
说明:测试提示信息

@Test
void testListHelp() {
 String[] args = {
         "list" ,
 };
 InfectStatistic.main(args);
}

listOut12
说明:测试list命令的提示信息

@Test
void testUnknownCmdError() {
 String[] args = {
         "listt" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut11.txt" ,
 };
 InfectStatistic.main(args);
}

listOut13
说明:测试错误命令

@Test
void testUnknownParamError() {
 String[] args = {
         "list" ,
         "-loge" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut12.txt" ,
 };
 InfectStatistic.main(args);
}

listOut14
说明:测试list命令的错误参数

@Test
void testLackParamError1() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" ,
 };
 InfectStatistic.main(args);
}

listOut15
说明:测试必要参数缺少参数值

@Test
void testLackParamError2() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
 };
 InfectStatistic.main(args);
}

listOut16
说明:测试缺少必要参数

@Test
void testUnknownType() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" , "D:/Java/InfectStatistic-main/test/result/listOut13.txt" ,
         "-type" , "确诊" , "sp" , "cure" ,
 };
 InfectStatistic.main(args);
}

listOut17
说明:测试-type参数的错误参数值

  • 单元测试覆盖率

覆盖率

6、性能测试,性能优化

  • 性能测试截图及说明

    • 说明

      性能测试用了2020-01-20到2010-01-31共十二个日志文件,每个日志文件近万条数据,共约十二万条日志数据
      使用Jprofiler11进行分析

    • 测试结果

      1
      2

  • 性能优化

    • 分析

      由测试结果可以看出程序再doDate()这个方法耗时最久,于是优先优化这个方法。
      发现我在处理每一条日志数据的时候都计算了全国数据,这没有必要,只需要输出的时候计算全国数据就可以。于是我修改了计算方法,并增加了一个用于计算全国数据的方法。

    private void countNational() {
     	List<Integer> national = statistics.get("全国");
     	for (List<Integer> data : statistics.values()) {
            for (int i = 0 ; i < national.size() ; i ++){
            national.set(i,national.get(i) + data.get(i));
    	}
    }
    

    这个方法在doOut()输出之前调用。
    doDate()是一个个读取log文件,计算完一个文件再读取下一个,我想为何不能用多线程来完成这一步骤呢?把读取文件内容并计算的任务交给线程去处理,充分发挥多核处理器的并发性能。

    threadPool.submit(() -> { //创建新线程,加入线程池
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(new FileInputStream(log), StandardCharsets.UTF_8));
            String dataRow;
            while ((dataRow = reader.readLine()) != null) {
                if (dataRow.startsWith("//")) { //忽略注释行
                    continue;
                }
                String[] data = dataRow.split(" ");  //分割数据行
                if (!outProvince.contains(data[0])) {
                    outProvince.add(data[0]);
                }
                synchronized (statistics) {   //线程同步,给数据加锁
                    List<Integer> provinceData = statistics.get(data[0]);   //当前行的省份数据
                    List<Integer> destProvince;   //用于处理流入
                    switch (data[1]) {
                        case INCREMENT:  //处理新增
                           if (data[2].equals(INFECTION_PATIENT)) {  //新增感染
                                increaseInf(provinceData, Lib.parseData(data[3]));
                            } else {                                  //新增疑似
                                increaseSus(provinceData, Lib.parseData(data[3]));
                            }
                            break;
                        case EXCLUDE:  //处理排除疑似
                            excludeSus(provinceData, Lib.parseData(data[3]));
                            break;
                        case CURE:  //处理治愈
                            cure(provinceData, Lib.parseData(data[2]));
                            break;
                        case DEAD:  //处理死亡
                            dead(provinceData, Lib.parseData(data[2]));
                            break;
                        case INFECTION_PATIENT:  //处理感染患者流入
                            destProvince = statistics.get(data[3]);
                            infInflow(provinceData, destProvince, Lib.parseData(data[4]));
                            break;
                        case SUSPECTED_PATIENT:
                            if (data[2].equals(INFLOW)) {   //处理疑似患者流入
                                destProvince = statistics.get(data[3]);
                                susInflow(provinceData, destProvince, Lib.parseData(data[4]));
                            } else if (data[2].equals(DIAGNOSE)) {  //处理确诊
                                diagnose(provinceData, Lib.parseData(data[3]));
                            }
                            break;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    threadPool.shutdown();
    while (!threadPool.isTerminated());   //等待所有线程执行完
    
    • 优化结果

      1
      2
      可以看到程序执行时间从1142ms优化到了972ms,文件数量越多的话优化效果越明显。由于要兼顾线程同步,耗费的时间并没有太大的改善,或许换成其他的线程加锁方式能改善许多。

7、心路历程与收获

刚开始看到作业的时候人是懵的,毕竟有太多知识是第一次接触到,比如PSP表格,单元测试等。后来去查资料,去认真的学习新的知识,我也对软件工程和项目开发过程也有了更多的了解。按照作业要求分析需求,搭建好了程序基本结构,发现看似复杂的任务也简单了起来。然后就是写代码,做测试,写代码,做测试。。。直到最终完成所有需求。开发中遇到了许多的bug,都在测试过程中很容易的解决掉了,也让我感受到在编程中测试发现bug解决起来比编程完成后的测试发现bug解决起来容易得多。最后性能测试的过程中使用了专门的性能分析工具,也让我更能看到代码中需要优化的部分,优化起来也方便的多。
总的来说这次作业收获还是很大的,学到了更多项目开发的知识,学到了单元测试和性能测试工具的使用,学到了GitHub和简单git指令的使用。学无止境,希望下次作业能收获更多。

posted @ 2020-02-16 14:24  HeroKilito  阅读(247)  评论(1编辑  收藏  举报