软件工程第二次作业--文件读取

222100308向至尚 2024-02-29 21:20:36
这个作业属于哪个课程福州大学-202302软件工程实践
这个作业要求在哪里软件工程第二次作业--文件读取
这个作业的目标实现一个对世界游泳锦标赛跳水项目相关赛事数据进行统计的控制台程序
其他参考文献Maven最全教程 --知乎Jackson使用详解Java 基础(四)IO 流之使用文件流的正确姿势JUnit 4 超详细教程

目录

  • 1. gitcode项目地址
  • 2. PSP表格
  • 3. 解题思路描述
  • 3.1 项目结构与依赖
  • 3.2 数据预处理
  • 3.3 文件的输入和输出
  • 3.4 测试性能
  • 3.5 进行单元测试
  • 3.6 打包成jar包
  • 4. 设计与实现功能
  • 4.1 pojo实体类介绍
  • 4.2 项目整体架构
  • 4.3 接口设计流程图
  • 4.4 算法独到之处
  • 5. 关键代码展示
  • 5.1 入口调用函数:
  • 5.2 获取运动员列表:
  • 5.3 得到单项比赛记录:
  • 5.4 打印详细比赛结果:
  • 5.5 在总比赛记录中添加单项比赛记录:
  • 5.6 异常处理
  • 6. 程序性能改进
  • 6.1 改进代码:
  • 6.2 改进结果展示(使用JProfiler)
  • 7. 单元测试展示
  • 8. 心路历程与收获

1. gitcode项目地址

仓库地址

2. PSP表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划3030
• Estimate• 估计这个任务需要多少时间3030
Development开发11951440
• Analysis• 需求分析 (包括学习新技术)300400
• Design Spec• 生成设计文档3040
• Design Review• 设计复审6050
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)2040
• Design• 具体设计4560
• Coding• 具体编码500550
• Code Review• 代码复审4050
• Test• 测试(自我测试,修改代码,提交修改)200250
Reporting报告100105
• Test Repor• 测试报告5060
• Size Measurement• 计算工作量3020
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划2025
合计13251575

3. 解题思路描述

3.1 项目结构与依赖

首先,在进行开发项目之前需要思考项目的依赖管理,我选择maven作为项目的依赖管理,maven可以通过编写pom.xml来实现依赖的管理,maven的具体介绍如下:maven,我通过以下内容学习如何使用maven:

我的项目结构如下:

├── common
│   ├── EventStage.java
│   └── SearchPattem.java
├── dao
│   ├── AthleteDAO.java
│   ├── EventRecordDAO.java
│   └── ParticipationDAO.java
├── DWASearch.java
├── pojo
│   ├── Athlete.java
│   ├── EventRecord.java
│   └── ParticipationRecord.java
└── utils
    ├── JsonUtils.java
    └── SearchUtils.java
  • common:存放一些常量,比如enum类等
  • pojo: 一些封好的java实体类
  • dao:提供对pojo实体类的数据操作
  • utils: 一些工具类

3.2 数据预处理

本次作业使用助教提前准备好的data数据,其结构如下:

├── athletes.json
├── event.json
├── readme.md
└── results
    ├── 108c795d-5e4f-4dc6-acea-0bc70bfd1928.json
    ├── 26f2e7ae-fc1a-4a5e-9653-858c1dc9e95f.json
    ├── 6119565c-9b6a-429a-ac0d-183a7fd37e55.json
    ├── 732633c2-7fcd-40f3-b3bc-203d8eec161c.json
    ├── 7b2f14f7-5e6b-44e0-8bb4-f78591e9545b.json
    ├── 7e6bbfb2-54c8-4083-845e-ca7fc6932d7f.json
    ├── 96662b5e-200e-4f05-9565-154456cd21e4.json
    ├── d545954e-8e8a-4841-8ab4-a5ea0e188380.json
    ├── e85aed35-be49-4251-9558-a4b651f57266.json
    └── f73a2ff2-96c1-4075-8a69-21d513aebfa9.json
  • athletes:存储了关于跳水项目的运动员信息。
  • event:存储了项目名称项目id的对照,通过项目名称可以找到对应的项目id。
  • results:文件夹下存储以项目id命名的各个项目的成绩。包括总分与每一次比赛的小分。

因此,首先要处理这些json数据将其转化为java中的对象,在转化json时查阅资料我选择了Jackson这个库

Jackson 是一个用于处理 JSON 数据的 Java 库,广泛用于 Java 应用程序中进行 JSON 数据的序列化和反序列化。

我通过以下内容学会了如何使用Jackson库:

通过使用其提供的JsonNode、ObjectMapper即可实现对json数据的提取。

3.3 文件的输入和输出

作业要求我们要将input文件中的指令读入然后再output中进行输出,我通过查阅资料了解了java中可以使用FileReader和BufferedReader实现文件输入,FileWriter和BufferedWriter实现文件输出,具体的操作我查阅了以下资料:

其中关键代码如下:

BufferedReader reader = new BufferedReader(new FileReader(inputFile));
BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));

同时注意到因为input文件内可能包含多条指令,因此需要按行存取指令,这里我使用了readLine()方法来处理,readLine的使用方法参考:java-readLine()用法

3.4 测试性能

作业要求要对代码进行性能测试,经过查阅资料我选择使用JProfiler性能分析器来测试,经过对Java JVM分析利器JProfiler 结合IDEA使用详细教程的研究,总结了使用方法:

  1. 在plugin中下载JProfiler插件

img

  1. 使用idea运行JProfiler

img

3.5 进行单元测试

单元测试我选择了JUnit库进行,使用了白盒测试编写十余个测试用例进行测试,查阅的资料如下:

打包jar包上,我查阅资料发现可以直接使用maven自带的打包功能,只需要选择Maven面板下Lifecycle中的package就可以进行打包,具体操作如下:如何通过maven打包可执行jar包

img

4. 设计与实现功能

4.1 pojo实体类介绍

img

  • Athelete类:运动员类,包括了姓名、性别、国家以及参赛项目等属性
  • EventRecord类:运动员单项参赛记录类,包括了运动员姓名、排名、比赛阶段和分数等属性,表示一个运动员参加的单次比赛记录
  • ParticipationRecord类: 运动员总比赛记录类,包括了运动员姓名、排名、预赛成绩、半决赛成绩以及决赛成绩,表示一个运动员在一个项目完整比赛周期获得的成绩记录

    4.2 项目整体架构

img

4.3 接口设计流程图

  1. 打印运动员信息:printPlayersMsg
    首先,在 SearchUtils 中维护静态的 List<Athlete> athleteList ,其中存储着运动员信息,通过静态代码块为其赋值,调用 AthleteDAO.getAthleteList() 得到运动员列表,该方法通过使用jackson库中的objectMapper.readTree来实现对json数据的提取,最后得到运动员列表后通过格式化打印String.format来实现输出。

img

  1. 打印比赛信息:printNormalResults
    首先通过JsonUtils.getEventId得到指定比赛项目字符串所对应的比赛id,该方法通过匹配event.json中的DisciplineName,取出其idid.json文件便是我们后续需要解析的文件。接着我们根据得到的文件调用JsonUtils.getEventResult,该方法也是通过jackson库中的objectMapper.readTree来提取信息,json中的HeatsdivesDivePoints都是我们需要提取的信息,将信息提取后我使用了List<Map<String, String>>来封装结果,最后也是使用String.format来实现输出。

img

  1. 打印详细比赛信息:printDetailedResults
    printNormalResults类似,首先通过JsonUtils.getEventResult得到预赛,半决赛和决赛的单项结果,通过对运动员列表athleteList中的disciplines参赛项目遍历,得到参加比赛的选手,将其信息和单项结果传入ParticipationDAO.addRecord封装,ParticipationDAO维护了一个静态的List<ParticipationRecord>,当插入的运动员比赛记录不存在时将会创建,存在时将会使用原先列表中的比赛信息。

img

4.4 算法独到之处

  • 项目结构清晰,将接口封装成pojo实体类、DAO数据控制层和utils工具层,实现将数据存储和检索的逻辑与应用程序的其他部分分离开来,便于项目维护。
  • 加入异常处理,在读取文件、解析JSON时加入异常处理机制,提高程序的健壮性。
  • 封装结果,将杂乱无章的json文件封装成不同的实体,便于具体功能的实现。

5. 关键代码展示

5.1 入口调用函数:

BufferedReader reader = new BufferedReader(new FileReader(inputFile));
            BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));
            String line;
            while ((line = reader.readLine()) != null) {
                if ("players".equals(line.split(" ")[0])) {
                    SearchUtils.printPlayersMsg(writer);
                } else if ("result".equals(line.split(" ")[0])) {
                    SearchUtils.printResults(writer,line.replace("result ",""));
                } else {
                    String errorMsg = String.format("Error%n-----%n");
                    writer.write(errorMsg);
                }

            }

5.2 获取运动员列表:

public static List<Athlete> getAthleteList() {
        ArrayList<Athlete> athletes = new ArrayList<>();
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            JsonNode countriesNode = objectMapper.readTree(new File("/home/eric/codes/swe/project-java/222100308/data/athletes.json"));
            // 取出每一个国家
            for (JsonNode countryNode : countriesNode) {
                String country = countryNode.get("CountryName").asText();
                // 取出每一个运动员
                for (JsonNode athleteNode : countryNode.get("Participations")) {
                    String fullName = athleteNode.get("PreferredLastName").asText() + " " +
                            athleteNode.get("PreferredFirstName").asText();
                    String gender = athleteNode.get("Gender").asInt() == 0 ? "Male" : "Female";
                    Athlete athlete = new Athlete(fullName,athleteNode.get("PreferredFirstName").asText(),athleteNode.get("PreferredLastName").asText(), gender, country);
                    // 获取运动员参赛项目
                    for (JsonNode eventNode : athleteNode.get("Disciplines")) {
                        athlete.addDiscipline(eventNode.get("DisciplineName").asText());
                    }
                    athletes.add(athlete);
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
        // 对运动员进行排序
        athletes.sort((athlete1, athlete2) -> {
            int countryComparison = athlete1.getCountry().compareTo(athlete2.getCountry());
            if (countryComparison != 0) {
                return countryComparison;
            }
            String name1 = athlete1.getLastName();
            String name2 = athlete2.getLastName();
            return name1.compareTo(name2);
        });
        return athletes;
    }

5.3 得到单项比赛记录:

// 将JSON文件转化为格式化后的结果
    public static List<Map<String, String>> getEventResult(String file,String mode) {
        file = "/home/eric/codes/swe/project-java/222100308/data/results/" + file + ".json";
        List<Map<String, String>> res = new ArrayList<>();
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            JsonNode rawFile= objectMapper.readTree(new File(file));
            JsonNode heats = getHeat(rawFile.get("Heats"),mode);
            if (heats != null) {
                JsonNode results = getHeat(rawFile.get("Heats"),mode).get("Results");
                for(JsonNode result : results) {
                    Map<String, String> r = new HashMap<>();
                    r.put("totalPoints",result.get("TotalPoints").asText());
                    JsonNode dives = result.get("Dives");
                    StringBuilder score = new StringBuilder();
                    for (JsonNode dive : dives) {
                        score.append(dive.get("DivePoints").asText()).append("+");
                    }
                    r.put("scores", String.valueOf(score));
                    r.put("rank",result.get("Rank").asText());
                    r.put("name",result.get("FullName").asText());
                    res.add(r);
                }
            }
            else {
                return null;
            }


        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return res;
    }

5.4 打印详细比赛结果:

    private static void printDetailedResults(Writer writer,String event) {
        // 陷在缓存中寻找指令
        List<ParticipationRecord> records = findCachedDetailedRecord(event);
        if (records != null) {
            System.out.println("缓存存在指令内容,直接读取---");
        } else {
            String eventId = JsonUtils.getEventId(event);
            List<String> participates = AthleteDAO.getParticipates(event, athleteList);
            // 得到单项比赛记录
            List<Map<String, String>> finalResult = JsonUtils.getEventResult(eventId, "Final");
            List<Map<String, String>> preliminaryResult = JsonUtils.getEventResult(eventId, "Preliminary");
            List<Map<String, String>> semifinalResult = JsonUtils.getEventResult(eventId, "Semifinal");
            if (finalResult != null) {
                // 遍历参赛选手
                for (String name : participates) {
                    EventRecord eventRecord = EventRecordDAO.getEventRecord(name, finalResult, EventStage.FINAL);
                    // 选手有参赛
                    if (eventRecord != null) {
                        // 总比赛记录加入单项记录
                        ParticipationDAO.addRecord(eventRecord);
                    }
                }
            }
            if (semifinalResult != null) {
                for (String name : participates) {
                    EventRecord eventRecord = EventRecordDAO.getEventRecord(name, semifinalResult,EventStage.SEMIFINAL);
                    // 选手有参赛
                    if (eventRecord != null) {
                        ParticipationDAO.addRecord(eventRecord);
                    }
                }
            }
            if (preliminaryResult != null) {
                for (String name : participates) {
                    EventRecord eventRecord = EventRecordDAO.getEventRecord(name, preliminaryResult,EventStage.PRELIMINARY);
                    // 选手有参赛
                    if (eventRecord != null) {
                        ParticipationDAO.addRecord(eventRecord);
                    }
                }
            }
            records = ParticipationDAO.getRecords();
            addCachedDetailedRecord(event,records);
            System.out.println("缓存中不存在,创建新缓存---");
        } for (ParticipationRecord record : records) {
            int[] ranks = record.getRanks();
            String fullName = record.getFullName();
            if (fullName.contains("/")) {
                String[] names = fullName.split("\\s*/\\s*");
                int result = names[0].compareTo(names[1]);
                if (result > 0) {
                    String temp = names[1];
                    names[1] = names[0];
                    names[0] = temp;
                }
                fullName = String.format("%s & %s",names[0],names[1]);
            }
            String rank = String.format("%s | %s | %s",ranks[0]!=-1.0 ? ranks[0]:"*",ranks[1]!=-1.0 ? ranks[1]:"*",ranks[2]!=-1.0 ? ranks[2]:"*");
            // 得到格式化后的比赛结果
            StringBuilder pScores = getScoreString(record,EventStage.PRELIMINARY);
            StringBuilder sScores = getScoreString(record,EventStage.SEMIFINAL);
            StringBuilder fScores = getScoreString(record,EventStage.FINAL);
            String output = String.format("Full Name:%s%nRank: %s%nPreliminary Score:%s%nSemifinal Score:%s%nFinal Score:%s%n-----%n",
                    fullName, rank, pScores,sScores,fScores);
            try {
                writer.write(output);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

        }
    }

5.5 在总比赛记录中添加单项比赛记录:

public static void addRecord(EventRecord record) {
        // 查找是否已经记录过单项比赛的记录
        ParticipationRecord participationRecord = findRecord(record.getName());
        // 没有记录则创建新记录
        if (participationRecord == null) {
            participationRecord = new ParticipationRecord(record.getName());
            records.add(participationRecord);
        }
        // 记录决赛记录
        if (record.getStage() == EventStage.FINAL) {
            if (participationRecord.getfScores(0) == -1.0) {
                for (int i = 0;i < record.getScoreLength();i++) {
                    participationRecord.setfScores(record.getScore(i),i);
                }
                participationRecord.setfScores(Double.parseDouble(record.getFinalScore()),record.getScoreLength());
                participationRecord.setRanks(Integer.parseInt(record.getRank()),2);
            }
        }
        // 记录半决赛记录
        if (record.getStage() == EventStage.SEMIFINAL) {
            if (participationRecord.getsScores(0) == -1.0) {
                for (int i = 0;i < record.getScoreLength();i++) {
                    participationRecord.setsScores(record.getScore(i),i);
                }
                participationRecord.setsScores(Double.parseDouble(record.getFinalScore()),record.getScoreLength());
                participationRecord.setRanks(Integer.parseInt(record.getRank()),1);
            }
        }
        // 记录预赛记录
        if (record.getStage() == EventStage.PRELIMINARY) {
            if (participationRecord.getpScores(0) == -1.0) {
                for (int i = 0;i < record.getScoreLength();i++) {
                    participationRecord.setpScores(record.getScore(i),i);
                }
                participationRecord.setpScores(Double.parseDouble(record.getFinalScore()),record.getScoreLength());
                participationRecord.setRanks(Integer.parseInt(record.getRank()),0);
            }
        }
    }

5.6 异常处理

  1. 指令错误处理:
if(args.length != 2) {
            System.out.println("指令错误");
            return;
        }
  1. 项目名称错误处理:
if (!"N/A".equals(validEvent)) {
            if (validEvent.contains("detail")) {
                printDetailedResults(writer,validEvent.replace(" detail",""));
            } else {
                printNormalResults(writer,validEvent);
            }
        }
        else {
            System.out.println("找不到该项目,请检查项目名称是否正确");
            try {
                String output = String.format("N/A%n-----%n");
                writer.write(output);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

6. 程序性能改进

由于初始的代码存在多次存取json文件的情况,导致每次执行指令都要重新读取json,因此我采取了缓存的策略,将读取的指令结果存入缓存的cachedDetailedRecord和cachedNormalRecord中,如果下次遇到相同的指令,可以直接取出缓存内容,减少访问时间。

6.1 改进代码:

  1. 缓存原型:
private static List<Map<String,List<ParticipationRecord>>> cachedDetailedRecord;
private static List<Map<String,List<Map<String, String>>>> cachedNormalRecord;
  1. 查找缓存记录:
private static List<ParticipationRecord> findCachedDetailedRecord(String event) {
        for (Map<String, List<ParticipationRecord>> entry : cachedDetailedRecord) {
            if (entry.containsKey(event)) {
                return entry.get(event);
            }
        }
        return null;
    }
  1. 增加缓存记录
private static void addCachedDetailedRecord(String event,List<ParticipationRecord> record) {
        Map<String, List<ParticipationRecord>> re = new HashMap<>();
        re.put(event,record);
        cachedDetailedRecord.add(re);
    }

6.2 改进结果展示(使用JProfiler)

我们统一随机生成了10w条指令数据进行性能分析

  1. 优化代码前:

img

可以看到优化代码前执行用时为115s
2. 优化代码后:

img

优化代码后执行用时为24s,性能有较大提升,同时也注意到由于使用了缓存机制,其内存占用率也相应增高。

7. 单元测试展示

单元测试我使用了junit模块,对打印运动员信息和打印比赛项目进行了测试,测试代码如下:

public class SearchUtilsTest {

    @Test
    public void printPlayersMsg() throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter("/home/eric/codes/swe/project-java/222100308/input.txt"));
        SearchUtils.printPlayersMsg(writer);
    }

    @Test
    public void printResults() throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter("/home/eric/codes/swe/project-java/222100308/output.txt"));
        String[] events = {
                "women 1m springboard",
                "women 3m synchronised",
                "men 1m springboard",
                "men 3m synchronised",
                "women 1m springboard detail",
                "women 3m synchronised detail",
                "men 3m springboard detail",
                "men 3m synchronised detail",
                "men 10m synchronised de",
                "men 1000m synchronised",
        };
        for (String event : events) {
            SearchUtils.printResults(writer,event);
        }
    }
}

测试结果代码覆盖率如下:

img

代码覆盖率均达到85%以上,符合单元测试标准

8. 心路历程与收获

在本次文件提取作业中,我深刻感悟到一个项目的完成并不是跑通代码这么简单,以前的大作业往往只需要提交源代码即可,这次的作业从规划到实现再到测试编写报告,完整的一遍流程让我意识到自己在许多方面都不够严谨,比如单元测试方面,之前都没做过类似的测试,也没有形成代码规范,这些知识都是在这次作业中逐渐学习理解,同时在编程能力方面我也获益颇丰,学会了如何用maven来管理项目,使用git来及时签入更改,学会使用性能测试工具等等,这次作业不仅提升我的技术能力,同时也让我对软工实践这门课有了更深的认识

...全文
93 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

122

社区成员

发帖
与我相关
我的任务
社区描述
FZU-SE
软件工程 高校
社区管理员
  • LinQF39
  • 助教-吴可仪
  • 一杯时间
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧