软件工程实践第二次作业——个人实战

文章目录

这个作业属于哪个课程 2022年福大-软件工程
这个作业要求在哪里 软件工程实践第二次作业——个人实战
这个作业的目标 学习PSP相关知识,单元测试和性能调优
其他参考文献

该作业完成对冬奥会的赛事数据的收集,并实现一个能够对国家排名及奖牌个数统计的控制台程序。

Gitcode项目地址

gitcode项目地址

PSP表格

PSP Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 120 120
• Estimate • 估计这个任务需要多少时间 2400 3600
Development • 开发 2100 3340
• Analysis • 需求分析 (包括学习新技术) 120 160
• Design Spec • 生成设计文档 240 240
• Design Review • 设计复审 60 180
• Coding Standard • 代码规范 (为目前的开发制定合适的规范) 60 40
• Design • 具体设计 60 180
• Coding • 具体编码 1000 1840
• Code Review • 代码复审 60 100
• Test • 测试(自我测试,修改代码,提交修改) 560 600
Reporting 报告 180 540
• Test Repor • 测试报告 120 480
• Size Measurement • 计算工作量 30 30
• Postmortem & Process Improvement Plan • 事后总结, 并提出过程改进计划 30 30
  合计 2400 4000

解题思路描述

Created with Raphaël 2.3.0 开始 读取输入文件 逐行解析命令 命令结果是否缓存 从缓存中获取数据 结果汇总并输出到文件中 结束 从数据源获取数据 yes no

如何解析json文件?

  • 调用第三方库,但需要选择最适合本项目的解析器,将对各种解析器分别进行单元测试,选择最适合的解析器作为项目默认json解析器,可选解析器有:Gson(最终选择)、FastDFS、Jackson。
  • 因为有多种解析器可供选择,所以可以使用设计模型解耦合,减少代码量,也易于单元测试。

如何获取DTO(数据传输对象)?即数据源如何选择?

如果不考虑完全缓存的话,目前有两种解决方案:

  • 调用冬奥API
    分析:GET请求API,中间涉及到发起网络请求。网络IO可能会是程序的瓶颈段,可以通过多线程异步请求提高吞吐量。因为使用多线程,所以需要线程池管理线程。
  • 读取本地数据
    分析:根据需求可知,需要的数据已经不会再更新了,因此可以将静态数据文件存放在本地中,在需要时调用操作系统IO读取文件即可。文件IO可能会是程序的瓶颈段,仍然可以通过多线程异步请求IO提高吞吐量,仍然需要线程池。

如何进行优化?

  • 根据数据源的两种选择策略可知,程序瓶颈段都会在IO处,所以可以考虑并发读取数据,设置线程池减少线程创建的性能消耗,因此优化时需要考虑核心线程数和选择最优的阻塞队列
  • 考虑到输入文件中可能会有重复的命令,因此可以使用缓存技术减少IO。但是还有一点很重要:如果input文件中重复的命令非常多,程序主要的瓶颈段会变成缓存查找,会转变为CPU密集型任务,核心线程数应该越少越好,减少线程切换次数。

如何实现缓存机制?

  • 基于LinkedHashMap实现LRU算法的缓存,效率会稍低于HashMap,但不会发生内存泄漏。

  • 使用HashMap,虽然使用HashMap会造成内存泄漏,但本程序非长期运行,只运行一次就结束,因此可以使用。但是会产生线程安全问题,所有需要使用ConcurrentHashMap

如何解决缓存带来的GC性能问题?

  • 可以借助性能分析工具调整堆的大小以及选择最优的垃圾回收算法等减少GC次数。

接口设计和实现过程

接口和抽象类的设计

Json解析器接口设计
软件工程实践第二次作业——个人实战

为了程序支持不同的Json解析器,使用适配器设计模式,将每个第三方库的反序列化方法统一成deserilize方法进行解耦合,不需要改动代码则可以切换解析器,也易于单元测试。
经过测试,GsonFactory是解析最快的解析工厂。点此查看测试结果

数据源接口设计:

软件工程实践第二次作业——个人实战

为支持多种数据源,设计出AbstractDataSource抽象类,封装解析命令的方法,而由实例类实现获取奖牌榜json字符串和赛况json字符串的方法。
经过测试,LocalDataSource会比RemoteDataSource提高了1334.6%的运行速度。点此查看测试结果

类图

软件工程实践第二次作业——个人实战

软件工程实践第二次作业——个人实战

关键代码展示

初始化启动参数

软件工程实践第二次作业——个人实战

在main函数启动时,初始化启动参数。会把输入文件和输出文件地址放入应用程序环境中,也会读取启动时设置的json解析器和数据源。

数据源抽象类解析命令逻辑

软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战

command由外部传入,可保证前后不含空格。
解析流程如下:

  1. 判断是否为total命令,不是则判断是否符合schedule命令格式。
  2. 判断schedule命令格式分为两步:判断格式正确、判断日期正确,这两次判断均使用正则表达式判断。
  3. 如果schedule命令格式也错误,则返回Error。
  4. 如果是total命令,则向数据源获取奖牌榜数据,解析后返回。
  5. 如果schedule命令格式正确,而日期不符合正常日期格式,则直接返回N/A,如果符合正常日期格式,则向数据源获取对应日期数据。
  6. 如果数据源返回对应日期数据为空,则说明输入日期不在冬奥会期间,没有赛况信息,返回N/A,否则使用数据源的日期数据进行解析后返回。

软件工程实践第二次作业——个人实战
如果判断命令为total命令,则调用数据源实现类的getTotalMedalsJson方法获取奖牌榜json数据并进行解析。

软件工程实践第二次作业——个人实战
如果判断命令为schedule命令,则检查格式和简单判断日期格式,如果均符合则调用数据源实现类的getMatchesJson方法获取赛况json数据并进行解析。

Lib类的单例模式实现

软件工程实践第二次作业——个人实战

处理输入文件

软件工程实践第二次作业——个人实战
使用try()自动关闭流;
使用线程池的submit方法传入Callable接口返回处理完command的字符串结果。

软件工程实践第二次作业——个人实战

在线程类中处理缓存,如果命令结果已缓存则直接返回缓存,没有则使用数据源解析命令并缓存。
缓存实现中存在线程安全问题,所以使用ConcurrentHashMap。
对key的处理这里去掉了所有的空格,可以减少重复意义的键值对
比如 schedule    0215和schedule 0215两种命令就可以用一个key:schedule0215所表示出来。


性能改进

使用多线程提高性能

测试环境:CPU 12核 、JVM采用默认参数、Gson解析器、本地数据源、线程池阻塞队列为100容量的ArrayBlockingQueue
测试数据:
软件工程实践第二次作业——个人实战
所有命令值各不相同,不会使用到缓存。

单线程执行:
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战

线程池核心线程数为CPU核数两倍:
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
单线程平均运行时间:210.67ms
多线程平均运行时间:131.00ms
提高了60.1%的运行速度。

选择最优阻塞队列:

ArrayBlockingQueue 队列大小为100

  • ArrayBlockingQueue测试结果如下
    软件工程实践第二次作业——个人实战
    软件工程实践第二次作业——个人实战
    软件工程实践第二次作业——个人实战
  • LinkedBlockingQueue测试结果如下
    软件工程实践第二次作业——个人实战软件工程实践第二次作业——个人实战
    软件工程实践第二次作业——个人实战
  • SynchronousQueue测试结果如下
    软件工程实践第二次作业——个人实战
    软件工程实践第二次作业——个人实战
    软件工程实践第二次作业——个人实战

可以看出三者的运行花费速度相差不大,但是考虑到ArrayBlockingQueue和LinkedBlockingQueue会创建队列项,而SynchronousQueue是直接传递任务,在任务数较少的情况下,会稍快一点(已经测试过了),所以最终选择的是SynchronousQueue阻塞队列

使用本地数据源提高性能

测试环境:CPU 12核 、JVM采用默认参数、Gson解析器、线程池阻塞队列为100容量的ArrayBlockingQueue、核心线程数为CPU核数两倍

远程数据源:
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战

本地数据源:
采用上面的数据。
远程数据源平均运行时间:1879.33ms
本地数据源平均运行时间:131.00ms
提高了1334.6%的运行速度。

使用缓存提高性能

缓存可以减少IO的次数,因此可以提高运行速度。

优化GC

测试环境:jvm启动参数如下
软件工程实践第二次作业——个人实战

设置堆的大小为20M,可知年轻代的大小为8M。打印GC日志如下:
软件工程实践第二次作业——个人实战

一共发生3次young gc,没有发生full gc,gc总耗费时间13.3ms。
jvisualvm可视化堆内存如下
软件工程实践第二次作业——个人实战
可知可以通过提高jvm堆内存大小减少gc次数。

将堆内存设为默认值:
软件工程实践第二次作业——个人实战

使用jvm默认参数值不会发生GC:
软件工程实践第二次作业——个人实战

软件工程实践第二次作业——个人实战
因此针对GC的改进方案是:使用jvm默认堆内存和GC收集器即可。

单元测试

测试各种json解析器解析json的速度:
软件工程实践第二次作业——个人实战
解析1000条json:
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
解析10条json:
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
解析1000000条json:
软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战
测试得出gson最适合本项目的json解析,因为本项目采用缓存,最高多解析次数即解析所有不同日期的赛况数据,数量比较小,所有采用gson作为默认json解析器。

测试命令的正则表达式:
软件工程实践第二次作业——个人实战

测试网络请求工具类:

软件工程实践第二次作业——个人实战

测试数据源:
软件工程实践第二次作业——个人实战

还有其他一些小的单元测试可以查看仓库源码。

异常处理

该项目有一个可能会出现的比较严重的异常,即启动应用时输入文件路径异常,该异常不能由程序修复,应抛出异常并让用户知道。

初始化应用环境时会检测输入文件路径是否异常,如果输入文件不存在,则会抛出异常并提示用户输入正确的输入文件路径。如果输出文件路径对应的文件不存在,会自动创建中间目录和文件。启动时输入的路径参数支持相对jar包的路径和绝对路径。

软件工程实践第二次作业——个人实战
软件工程实践第二次作业——个人实战

心得体会

通过本次作业:我学习到了

  • 从设计角度上:我了解到了PSP表格如何填写,以及第一次实现项目和文档共同构建。
  • 从工程角度上:
    • 我更理解了各种设计模式对应用扩展性和健壮性起到的重要作用。
    • 复习了许多Java基础的API。
    • 这也是我第一次将学习到的jvm相关的知识用在了项目中,虽然jvm的默认参数已经是最优的了,但我还是清楚了jvm调优的工作流程,相信在以后的学习工作中会发挥更大的用处。
  • 从现实角度上:我更理解了工作中会遇到的与甲方相处的过程:需求的变化和具体化让程序员不停地修改代码与文档。
上一篇:Json数据交互


下一篇:使用Flask开发简单接口