1. 實(shí)現(xiàn)一個(gè)小輪子,用AOP實(shí)現(xiàn)異步上傳

        共 13392字,需瀏覽 27分鐘

         ·

        2022-07-23 15:10

        往期熱門文章:
        1、聊聊接口優(yōu)化的幾種方法
        2、多線程使用不當(dāng)導(dǎo)致的 OOM
        3、老板要我開發(fā)一個(gè)簡(jiǎn)單的工作流引擎
        4、Spring Boot 啟動(dòng)時(shí)自動(dòng)執(zhí)行代碼的幾種方式,還有誰不會(huì)??
        5、Lombok原理和同時(shí)使?@Data和@Builder 的坑
        juejin.cn/post/7102343528525037576

        前言

        相信很多系統(tǒng)里都有這一種場(chǎng)景:用戶上傳Excel,后端解析Excel生成相應(yīng)的數(shù)據(jù),校驗(yàn)數(shù)據(jù)并落庫(kù)。這就引發(fā)了一個(gè)問題:如果Excel的行非常多,或者解析非常復(fù)雜,那么解析+校驗(yàn)的過程就非常耗時(shí)。如果接口是一個(gè)同步的接口,則非常容易出現(xiàn)接口超時(shí),進(jìn)而返回的校驗(yàn)錯(cuò)誤信息也無法展示給前端,這就需要從功能上解決這個(gè)問題。一般來說都是啟動(dòng)一個(gè)子線程去做解析工作,主線程正常返回,由子線程記錄上傳狀態(tài)+校驗(yàn)結(jié)果到數(shù)據(jù)庫(kù)。同時(shí)提供一個(gè)查詢頁面用于實(shí)時(shí)查詢上傳的狀態(tài)和校驗(yàn)信息。
        、多線程處理導(dǎo)入excel
        進(jìn)一步的,如果我們每一個(gè)上傳的任務(wù)都寫一次線程池異步+日志記錄的代碼就顯得非常冗余。同時(shí),非業(yè)務(wù)代碼也侵入了業(yè)務(wù)代碼導(dǎo)致代碼可讀性下降。從通用性的角度上講,這種業(yè)務(wù)場(chǎng)景非常適合模板方法的設(shè)計(jì)模式。即設(shè)計(jì)一個(gè)抽象類,定義上傳的抽象方法,同時(shí)實(shí)現(xiàn)記錄日志的方法,例如:
        //偽代碼,省略了一些步驟
        @Slf4j
        public abstract class AbstractUploadService<T{
           public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("-upload-pool-%d")
              .setPriority(Thread.NORM_PRIORITY).build();
           public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(1020300L,
              TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());

           protected abstract String upload(List<T> data);

           protected void execute(String userName, List<T> data) {
              // 生成一個(gè)唯一編號(hào)
              String uuid = UUID.randomUUID().toString().replace("-""");
              uploadExecuteService.submit(() -> {
                 // 記錄日志
                 writeLogToDb(uuid, userName, updateTime, "導(dǎo)入中");
                 // 一個(gè)字符串,用于記錄upload的校驗(yàn)信息
                 String errorLog = "";
                 //執(zhí)行上傳
                 try {
                    errorLog = upload(data);
                    writeSuccess(uuid, "導(dǎo)入中", updateTime);
                 } catch (Exception e) {
                    LOGGER.error("導(dǎo)入錯(cuò)誤", e);
                    //計(jì)入導(dǎo)入錯(cuò)誤日志
                    writeFailToDb(uuid, "導(dǎo)入失敗", e.getMessage(), updateTime);
                 }
                 /**
                  * 檢查一下upload是不是返回了錯(cuò)誤日志,如果有,需要注意記錄
                  *
                  * 因?yàn)殄e(cuò)誤日志可能比較長(zhǎng),
                  * 可以寫入一個(gè)文件然后上傳到公司的文件服務(wù)器,
                  * 然后在查看結(jié)果的時(shí)候允許用戶下載該文件,
                  * 這里不展開只做示意
                  */

                 if (StringUtils.isNotEmpty(errorLog)) {
                    writeFailToDb(uuid, "導(dǎo)入失敗", errorLog, updateTime);
                 }

              });
           }
        }
        如上文所示,模板方法的方式雖然能夠極大地減少重復(fù)代碼,但是仍有下面兩個(gè)問題:
        • upload方法得限定死參數(shù)結(jié)構(gòu),一旦有變化,不是很容易更改參數(shù)類型or數(shù)量
        • 每個(gè)上傳的service還是要繼承一下這個(gè)抽象類,還是不夠簡(jiǎn)便和優(yōu)雅
        為解決上面兩個(gè)問題,我也經(jīng)常進(jìn)行思考,結(jié)果在某次自定義事務(wù)提交or回滾的方法的時(shí)候得到了啟發(fā)。這個(gè)上傳的邏輯過程和事務(wù)提交的邏輯過程非常像,都是在實(shí)際操作前需要做初始化操作,然后在異?;蛘叱晒Φ臅r(shí)候做進(jìn)一步操作。這種完全可以通過環(huán)裝切面的方式實(shí)現(xiàn),由此,我寫了一個(gè)小輪子給團(tuán)隊(duì)使用。(當(dāng)然了,這個(gè)小輪子在本人所在的大團(tuán)隊(duì)內(nèi)部使用的很好,但是不一定適合其他人,但是思路一樣,大家可以擴(kuò)展自己的功能)
        「多說無益,上代碼!」

        代碼與實(shí)現(xiàn)

        首先定義一個(gè)日志實(shí)體
        public class FileUploadLog {
           private Integer id;
            // 唯一編碼
            private String batchNo;
            // 上傳到文件服務(wù)器的文件key
            private String key;
            // 錯(cuò)誤日志文件名
            private String fileName;
            //上傳狀態(tài)
            private Integer status;
            //上傳人
            private String createName;
            //上傳類型
            private String uploadType;
            //結(jié)束時(shí)間
            private Date endTime;
            // 開始時(shí)間
            private Date startTime;
        }
        然后定義一個(gè)上傳的類型枚舉,用于記錄是哪里操作的
        public enum UploadType {
           未知(1,"未知"),
           類型2(2,"類型2"),
           類型1(3,"類型1");
           
           private int code;
           private String desc;
           private static Map<Integer, UploadType> map = new HashMap<>();
           static {
              for (UploadType value : UploadType.values()) {
                 map.put(value.code, value);
              }
           }

           UploadType(int code, String desc) {
              this.code = code;
              this.desc = desc;
           }

           public int getCode() {
              return code;
           }

           public String getDesc() {
              return desc;
           }

           public static UploadType getByCode(Integer code) {
              return map.get(code);
           }
        }
        最后,定義一個(gè)注解,用于標(biāo)識(shí)切點(diǎn)
        @Retention(RetentionPolicy.RUNTIME)
        @Target({ElementType.METHOD})
        public @interface Upload {
           // 記錄上傳類型
           UploadType type() default UploadType.未知;
        }
        然后,編寫切面
        @Component
        @Aspect
        @Slf4j
        public class UploadAspect {
           public static ThreadFactory commonThreadFactory = new ThreadFactoryBuilder().setNameFormat("upload-pool-%d")
              .setPriority(Thread.NORM_PRIORITY).build();
           public static ExecutorService uploadExecuteService = new ThreadPoolExecutor(1020300L,
              TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024), commonThreadFactory, new ThreadPoolExecutor.AbortPolicy());


           @Pointcut("@annotation(com.aaa.bbb.Upload)")
           public void uploadPoint() {}

           @Around(value = "uploadPoint()")
           public Object uploadControl(ProceedingJoinPoint pjp) {
               // 獲取方法上的注解,進(jìn)而獲取uploadType
              MethodSignature signature = (MethodSignature)pjp.getSignature();
              Upload annotation = signature.getMethod().getAnnotation(Upload.class);
              UploadType type = annotation == null ? UploadType.未知 : annotation.type();
              // 獲取batchNo
              String batchNo = UUID.randomUUID().toString().replace("-""");
              // 初始化一條上傳的日志,記錄開始時(shí)間
              writeLogToDB(batchNo, type, new Date)
              // 線程池啟動(dòng)異步線程,開始執(zhí)行上傳的邏輯,pjp.proceed()就是你實(shí)現(xiàn)的上傳功能
              uploadExecuteService.submit(() -> {
                 try {
                    String errorMessage = pjp.proceed();
                    // 沒有異常直接成功
                    if (StringUtils.isEmpty(errorMessage)) {
                        // 成功,寫入數(shù)據(jù)庫(kù),具體不展開了
                        writeSuccessToDB(batchNo);
                    } else {
                        // 失敗,因?yàn)榉祷亓诵r?yàn)信息
                        fail(errorMessage, batchNo);
                    }
                 } catch (Throwable e) {
                    LOGGER.error("導(dǎo)入失?。?, e);
                    // 失敗,拋了異常,需要記錄
                    fail(e.toString(), batchNo);
                 }
              });
              return new Object();
           }

           private void fail(String message, String batchNo) {
               // 生成上傳錯(cuò)誤日志文件的文件key
              String s3Key = UUID.randomUUID().toString().replace("-""");
              // 生成文件名稱
              String fileName = "錯(cuò)誤日志_" +
                 DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH時(shí)mm分ss秒") + ExportConstant.txtSuffix;
              String filePath = "/home/xxx/xxx/" + fileName;
              // 生成一個(gè)文件,寫入錯(cuò)誤數(shù)據(jù)
              File file = new File(filePath);
              OutputStream outputStream = null;
              try {
                 outputStream = new FileOutputStream(file);
                 outputStream.write(message.getBytes());

              } catch (Exception e) {
                 LOGGER.error("寫入文件錯(cuò)誤", e);
              } finally {
                 try {
                    if (outputStream != null)
                       outputStream.close();
                 } catch (Exception e) {
                    LOGGER.error("關(guān)閉錯(cuò)誤", e);
                 }
              }
              // 上傳錯(cuò)誤日志文件到文件服務(wù)器,我們用的是s3
              upFileToS3(file, s3Key);
              // 記錄上傳失敗,同時(shí)記錄錯(cuò)誤日志文件地址到數(shù)據(jù)庫(kù),方便用戶查看錯(cuò)誤信息
              writeFailToDB(batchNo, s3Key, fileName);
              // 刪除文件,防止硬盤爆炸
              deleteFile(file)
           }

        }
        至此整個(gè)異步上傳功能就完成了,是不是很簡(jiǎn)單?(笑)
        那么怎么使用呢?更簡(jiǎn)單,只需要在service層加入注解即可,頂多就是把錯(cuò)誤信息return出去。
        @Upload(type = UploadType.類型1)
        public String upload(List<ClassOne> items)  {
           if (items == null || items.size() == 0) {
              return;
           }
           //校驗(yàn)
           String error = uploadCheck(items);
           if (StringUtils.isNotEmpty) {
               return error;
           }
           //刪除舊的
           deleteAll();
           //插入新的
           batchInsert(items);
        }

        結(jié)語

        寫了個(gè)小輪子提升團(tuán)隊(duì)整體開發(fā)效率感覺真不錯(cuò)。程序員的最高品質(zhì)就是解放雙手(偷懶?),然后成功的用自己寫的代碼把自己干畢業(yè)。。。。。。

        最近熱文閱讀:

        1、聊聊接口優(yōu)化的幾種方法
        2、面試官 | Spring Boot 項(xiàng)目如何統(tǒng)一結(jié)果,統(tǒng)一異常,統(tǒng)一日志?
        3、為什么不建議使用ON DUPLICATE KEY UPDATE?
        4、Java8 Stream,過分絲滑!
        5、8 種最坑SQL語法,工作中踩過嗎?
        6、Java 語言“坑爹” TOP 10
        7、你還不明白如何解決分布式Session?看這篇就夠了!
        8、能解決 80% 故障的排查思路
        9、程序員坐牢了,會(huì)被安排寫代碼嗎?
        10、面試被問Nginx,怎么破?
        關(guān)注公眾號(hào),你想要的Java都在這里

        瀏覽 34
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 操屄视频在线播放 | 人人插人人操人人射 | 做爰猛烈叫床视频 | 美女被操爽 | 97久久久久久久久 |