天天看點

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
Design 具體設計 150 100
Coding 具體編碼 420 540
Code Review 代碼複審
Test 測試(自我測試,修改代碼,送出修改) 120
Reporting 報告
Test Report 測試報告
Size Measurement 計算工作量
Postmortem & Process Improvement Plan 事後總結, 并提出過程改進計劃
合計

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為資料清單儲存該省份四種資料,每個資料項用整型存儲。

    • 主要函數流程圖
      2020寒假作業(2/2)

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);
}
           
2020寒假作業(2/2)
說明:基本參數的測試,隻有-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);
}
           
2020寒假作業(2/2)
說明:基本參數的測試,測試參數順序是否會影響結果
@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);
}
           
2020寒假作業(2/2)
說明:-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);
}
           
2020寒假作業(2/2)
說明:-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);
}
           
2020寒假作業(2/2)
說明:測試-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);
}
           
2020寒假作業(2/2)
說明:測試-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);
}
           
2020寒假作業(2/2)
說明:測試-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);
}
           
2020寒假作業(2/2)
@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);
}
           
2020寒假作業(2/2)
說明:測試全部參數
@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);
}
           
2020寒假作業(2/2)
@Test
void testHelp() {
 String[] args = {
 };
 InfectStatistic.main(args);
}
           
2020寒假作業(2/2)
說明:測試提示資訊
@Test
void testListHelp() {
 String[] args = {
         "list" ,
 };
 InfectStatistic.main(args);
}
           
2020寒假作業(2/2)
說明:測試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);
}
           
2020寒假作業(2/2)
說明:測試錯誤指令
@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);
}
           
2020寒假作業(2/2)
說明:測試list指令的錯誤參數
@Test
void testLackParamError1() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
         "-out" ,
 };
 InfectStatistic.main(args);
}
           
2020寒假作業(2/2)
說明:測試必要參數缺少參數值
@Test
void testLackParamError2() {
 String[] args = {
         "list" ,
         "-log" , "D:/Java/InfectStatistic-main/test/log" ,
 };
 InfectStatistic.main(args);
}
           
2020寒假作業(2/2)
說明:測試缺少必要參數
@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);
}
           
2020寒假作業(2/2)
說明:測試-type參數的錯誤參數值
  • 單元測試覆寫率

2020寒假作業(2/2)

6、性能測試,性能優化

  • 性能測試截圖及說明

    • 說明

      性能測試用了2020-01-20到2010-01-31共十二個日志檔案,每個日志檔案近萬條資料,共約十二萬條日志資料

      使用Jprofiler11進行分析

    • 測試結果
      2020寒假作業(2/2)
      2020寒假作業(2/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());   //等待所有線程執行完
               
    • 優化結果
      2020寒假作業(2/2)
      2020寒假作業(2/2)
      可以看到程式執行時間從1142ms優化到了972ms,檔案數量越多的話優化效果越明顯。由于要兼顧線程同步,耗費的時間并沒有太大的改善,或許換成其他的線程加鎖方式能改善許多。

7、心路曆程與收獲

剛開始看到作業的時候人是懵的,畢竟有太多知識是第一次接觸到,比如PSP表格,單元測試等。後來去查資料,去認真的學習新的知識,我也對軟體工程和項目開發過程也有了更多的了解。按照作業要求分析需求,搭建好了程式基本結構,發現看似複雜的任務也簡單了起來。然後就是寫代碼,做測試,寫代碼,做測試。。。直到最終完成所有需求。開發中遇到了許多的bug,都在測試過程中很容易的解決掉了,也讓我感受到在程式設計中測試發現bug解決起來比程式設計完成後的測試發現bug解決起來容易得多。最後性能測試的過程中使用了專門的性能分析工具,也讓我更能看到代碼中需要優化的部分,優化起來也友善的多。

總的來說這次作業收獲還是很大的,學到了更多項目開發的知識,學到了單元測試和性能測試工具的使用,學到了GitHub和簡單git指令的使用。學無止境,希望下次作業能收獲更多。