2020软工实践第一次个人编程作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/SE2020
这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/SE2020/homework/11167
这个作业的目标 总结软工的第一次个人编程作业
学号 031802223

一、PSP表格

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

二、解题思路

题目要求我们从GitHub用户行为数据中统计出每个人的、每个项目的、以及每个人的每个项目的四种事件的数量,并且需要存储结果,因此我打算将统计结果存入json文件中,需要查询时直接解析。

  • 任务要求与技术选择
    任务首先得有文件读取,其次是解析json串,将提取出的数据保存起来,最后还得解析main函数传入的参数,了解了项目需要的依赖就可以导包了,文件读取与写入我选择了commons-io有丰富的封装好的io操作,json的序列化和反序列化以及参数解析我分别选择了fastjson和common-cli有文档的直接查了文档就可以开始用了,没文档的上百度查例子学着用。
  • 具体思路
    先通过commons-cli解析命令行参数,进而决定需要执行什么操作。如果是初始化操作,就使用commons-io读取文件夹中的json文件并将文件存入HashMap中,统计完毕后,将所有HashMap序列化后存入各自的json文件中。如果是查询操作,只需要读取对应的json文件输出结果即可。

三、设计实现过程

项目初始为单线程版本,后迭代为多线程版本。

1.单线程版本

只有Main和Result两个类,所有业务写在Main中,逻辑简单,没有进行单元测试,效率也很一般

2.多线程版本

为了提高初始化统计数据的效率,我使用了多线程来初始化。于是多了类FileHandlerThread,用于解析json文件

  • 初始化过程
    主类会获取文件夹中的文件名的列表,创建三个线程,分别分配下标0,1,2,每次执行完后下标自动+3,当下标超过列表大小后,停止解析。每个线程对应一个json文件,统计结果会存入主类的concurrentHashMap中,三个线程的hashMap是同步的。最后由主类将hashMap序列化存入json文件中
  • 关键流程(init()方法)
  • 不足之处:且每个线程只能解析属于自己下标类的文件,比如线程1只能解析下标为0、3、6...的文件,如果这些下标的文件相比其他文件大小区别较大的话,就会出现两个线程空闲着而不帮另一个线程解析文件,此时效率相比单线程几乎没有提升。
  • 我考虑过使用AtomicInteger作为公共的文件下标,每个线程使用一次后自动+1,但是这样会出现同一个下标的文件被解析两次的情况,如果锁住该变量,又会出现后一个线程必须等待前一个线程执行完毕才能执行的情况,此时效率难以得到保证,于是我舍弃了这个想法,维持了一开始的做法。

四、代码说明

1.main()方法

    public static void main(String[] args) {
        //解析参数
        Options options = new Options();
        options.addOption("i","init",true,"pathToData")
                .addOption("u","user",true,"username")
                .addOption("e","event",true,"eventType")
                .addOption("r","repo",true,"repoName");
        CommandLineParser parser = new DefaultParser();
        try {
            CommandLine cmd = parser.parse( options, args);
            //如果参数中存在-i说明是初始化操作
            if(cmd.hasOption('i')){
                String path = cmd.getOptionValue('i');
                init(path);
            }else {//参数中未存在i说明是查询操作
                String eventType = cmd.getOptionValue('e');
                if(eventType!=null) {
                    //如果有-u参数
                    if (cmd.hasOption('u')) {
                        String username = cmd.getOptionValue('u');
                        //如果有-u参数还有-r参数说明要查询某个用户某个项目的事件数量
                        if (cmd.hasOption('r')) {
                            String repoName = cmd.getOptionValue('r');
                            System.out.println(getPersonalAndRepoThings(username,repoName,eventType));
                        } else {//如果没有-r参数说明只查询了某个用户的事件数量
                            System.out.println(getPersonalThings(username,eventType));
                        }
                    } else {//如果没有-u参数说明只查询了某个项目的事件数量
                        String repoName = cmd.getOptionValue('r');
                        System.out.println(getRepoThings(repoName,eventType));
                    }
                }
            }
        } catch (ParseException | IOException e) {
            e.printStackTrace();
        }
    }
  • 通过commons-cli的DefaultParser类和Option类解析参数,并根据不同的传参情况调用不同的方法。

2.init()方法

    static void init(String path){
        try {
            File file = new File(path);
            //获得文件夹中的文件名的集合
            List<String> fileNames = Arrays.asList(Objects.requireNonNull(file.list()));
            ///创建三个线程共同解析文件夹中的json文件
            FileHandleThread fileHandleThread1 = new FileHandleThread(userToResult,repoToResult,userAndRepoToResult,fileNames,path,0);
            FileHandleThread fileHandleThread2 = new FileHandleThread(userToResult,repoToResult,userAndRepoToResult,fileNames,path,1);
            FileHandleThread fileHandleThread3 = new FileHandleThread(userToResult,repoToResult,userAndRepoToResult,fileNames,path,2);
            Thread thread = new Thread(fileHandleThread1);
            Thread thread1 = new Thread(fileHandleThread2);
            Thread thread2 = new Thread(fileHandleThread3);
            thread.start();
            thread1.start();
            thread2.start();
            thread.join();
            thread1.join();
            thread2.join();
            //将三个map中的数据存入各自的json文件中,实现持久化
            FileUtils.writeStringToFile(new File("userToResult.json"), JSON.toJSONString(userToResult),"UTF-8",false);
            FileUtils.writeStringToFile(new File("repoToResult.json"), JSON.toJSONString(repoToResult),"UTF-8",false);
            FileUtils.writeStringToFile(new File("userAndRepoToResult.json"), JSON.toJSONString(userAndRepoToResult),"UTF-8",false);
        } catch (InterruptedException | IOException e) {
            e.printStackTrace();
        }
  • init()方法直接显式创建并运行三个线程,各自解析彼此不同的下标下的json文件,main的主线程需要在子线程解析完毕后再对map序列化,因此得join()三个线程,保证线程间的同步。
  • fastjson序列化对象成json串时,需要对象的类实现了getter()和setter(),我之前使用了lombok注解自动生成了这些方法,后来发现不起作用了(应该是插件的问题),后来我老实写了这些方法,就把序列化失败的问题解决了。

3.FileHandlerThread的run()方法

public void run() {
        while(num<fileNum){
            try {
                //获得本次解析的文件名
                String fileName = fileNames.get(num);
                //得是json文件才能被解析
                if(fileName.endsWith(".json")) {
                    //获得文件的行迭代器,可以逐行读取文件
                    LineIterator it = FileUtils.lineIterator(new File(path + "/" + fileName), "UTF-8");
                    //如果行迭代器还未到文件结尾
                    while (it.hasNext()) {
                        //解析当前迭代器的一行,迭代器自动往下移动一行
                        JSONObject jsonObject = JSONObject.parseObject(it.nextLine());
                        //获得json的type属性
                        String type = jsonObject.getString("type");
                        //事件类型应该是四种事件中的一种
                        if (typeCorrect(type)) {
                            //获得actor的login属性
                            String username = jsonObject.getJSONObject("actor").getString("login");
                            Result userResult = userToResult.get(username);
                            //先要判断集合中该用户的结果对象是否有被创建
                            if (userResult == null) {
                                //没有创建需要创建
                                userResult = new Result();
                                //增加该结果对象的对应事件的数量
                                userResult.inc(type);
                                //将新创建的对象放回map中
                                userToResult.put(username, userResult);
                            } else {
                                //如果集合中已经存在了该用户的对象,直接增加该结果对象的对应事件的数量即可
                                userResult.inc(type);
                            }
                            //同上,只是map的键变成了项目名repoName
                            String repoName = jsonObject.getJSONObject("repo").getString("name");
                            Result repoResult = repoToResult.get(repoName);
                            if (repoResult == null) {
                                repoResult = new Result();
                                repoResult.inc(type);
                                repoToResult.put(repoName, repoResult);
                            } else {
                                repoResult.inc(type);
                            }
                            //同上,只是map的键变成了用户名+项目名
                            String name = username + "_" + repoName;
                            Result result = userAndRepoToResult.get(name);
                            if (result == null) {
                                result = new Result();
                                result.inc(type);
                                userAndRepoToResult.put(name, result);
                            } else {
                                result.inc(type);
                            }
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            //执行完毕后(无论有无报错)下标自动加3,保证三个线程互不干扰,各自解析属于自己下标的json文件
            num+=3;
        }
    }

线程类继承了Runnable接口,每个线程对象中保存了当前解析的文件的下标,当下标等于或超过文件列表大小,就结束解析。LineIterator可以对文件逐行读取,每次读取一行的json数据,使用fastjson中JsonObject类的静态方法解析json,对题目要求的数据进行统计,最后存入主类的concurrentHashMap中。

五、单元测试

1.单元测试截图和描述

单元测试对初始化和三个查询的方法进行了测试,还测试了传入参数不合法的情况

  • 测试截图


打印了异常属于意料之内的结果,所以测试均符合预期。

2.单元测试覆盖率

  • 单元测试覆盖率达到了79%(不知道为啥这么低)

3.性能优化截图和描述

  • 使用的测试数据为3个相同的json文件,每个大小都约为500MB,总共1500MB,初始化init()占据了主要时间,也只有init()能被优化
  1. 单线程执行时间

    单线程情况下大约花费10500ms ~ 11000ms
  2. 多线程执行时间

    多线程情况下花费了5000ms~5700ms,明显对比得到多线程情况下初始化时间能减少近50%
  3. 多线程可能出现的问题
  • 使用多线程就不可避免的会出现线程不同步的问题,比如不同线程同时对同一个主类的map进行put操作时,就会有并发问题,对此我使用了ConcurrentHashMap代替了普通的HashMap,从而保证了put方法的原子性。
  • 当不同线程同时修改同一个结果对象的属性时,同样会因为并发导致有的值未修改成功,最后导致统计的数量较真实的数量少,我使用了synchronized修饰了Result类的inc方法, 杜绝了不同线程同时获得result对象控制权的情况。

六、代码规范链接

https://github.com/DQbryant/2020-personal-java/blob/master/codestyle.md

七、作业总结

  • 如果之前有一些Java和Python基础的话,这次编程作业应该没有太大问题,只要会调第三方库很容易就能搞定基本内容,使用第三方库意味着我需要学会如何查看文档,这次编程作业锻炼了我查文档看文档的能力。
  • 这次作业要求我们提交到gitHub,并用gradle代替maven,所以我在b站上自学了这两部分的内容。
  • 多线程部分由于我许久未用,且了解不深,使用不熟练,花费了我不少时间,而且没用上线程池,对多线程的使用很不到位,这次作业后我会自学java juc的内容,希望在今后项目上能派上用场。
  • 有一段时间没写过普通的Java程序了,这次作业让我必须回顾以前学过的内容,也锻炼了我解决问题的能力,相信之后再写Java程序就不会那么生疏。
原文地址:https://www.cnblogs.com/031802223-ldq/p/13669406.html