这个作业属于哪个课程 | 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
-
本次作业仓库地址
-
第一次作业相关仓库
- Spring
简介:MyBatis-Spting适配器,会帮助你将 MyBatis 代码无缝地整合到 Spring 中。 - Spring-framework
简介:Spring框架,Spring提供了Java编程语言以外的所有所需内容,可用于为各种场景和体系结构创建企业应用程序。 - mybatis-3
简介:对象关系映射工具,简单性是MyBatis数据映射器的最大优势。 - Spring-boot
简介:Spring Boot使创建具有Spring动力的生产级应用程序和服务变得非常容易。 - SpringCloudLearning
简介:SpringCloud教程
- Spring
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、主要代码
-
代码说明
- 开发语言:Java
- JDK版本:1.8
- 开发环境:IDEA
-
代码规范
https://github.com/herokilito/InfectStatistic-main/blob/master/221701328/codestyle.md
-
主要代码展示及说明
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); }
说明:基本参数的测试,只有-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); }
说明:基本参数的测试,测试参数顺序是否会影响结果
@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); }
说明:-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); }
说明:-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); }
说明:测试-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); }
说明:测试-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); }
说明:测试-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); }
说明:测试-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); }
说明:测试全部参数
@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); }
说明:测试全部参数
@Test void testHelp() { String[] args = { }; InfectStatistic.main(args); }
说明:测试提示信息
@Test void testListHelp() { String[] args = { "list" , }; InfectStatistic.main(args); }
说明:测试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); }
说明:测试错误命令
@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); }
说明:测试list命令的错误参数
@Test void testLackParamError1() { String[] args = { "list" , "-log" , "D:/Java/InfectStatistic-main/test/log" , "-out" , }; InfectStatistic.main(args); }
说明:测试必要参数缺少参数值
@Test void testLackParamError2() { String[] args = { "list" , "-log" , "D:/Java/InfectStatistic-main/test/log" , }; InfectStatistic.main(args); }
说明:测试缺少必要参数
@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); }
说明:测试-type参数的错误参数值
-
单元测试覆盖率
6、性能测试,性能优化
-
性能测试截图及说明
-
说明
性能测试用了2020-01-20到2010-01-31共十二个日志文件,每个日志文件近万条数据,共约十二万条日志数据
使用Jprofiler11进行分析 -
测试结果
-
-
性能优化
-
分析
由测试结果可以看出程序再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()); //等待所有线程执行完
-
优化结果
可以看到程序执行时间从1142ms优化到了972ms,文件数量越多的话优化效果越明显。由于要兼顾线程同步,耗费的时间并没有太大的改善,或许换成其他的线程加锁方式能改善许多。
-
7、心路历程与收获
刚开始看到作业的时候人是懵的,毕竟有太多知识是第一次接触到,比如PSP表格,单元测试等。后来去查资料,去认真的学习新的知识,我也对软件工程和项目开发过程也有了更多的了解。按照作业要求分析需求,搭建好了程序基本结构,发现看似复杂的任务也简单了起来。然后就是写代码,做测试,写代码,做测试。。。直到最终完成所有需求。开发中遇到了许多的bug,都在测试过程中很容易的解决掉了,也让我感受到在编程中测试发现bug解决起来比编程完成后的测试发现bug解决起来容易得多。最后性能测试的过程中使用了专门的性能分析工具,也让我更能看到代码中需要优化的部分,优化起来也方便的多。
总的来说这次作业收获还是很大的,学到了更多项目开发的知识,学到了单元测试和性能测试工具的使用,学到了GitHub和简单git指令的使用。学无止境,希望下次作业能收获更多。