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

        共 13381字,需瀏覽 27分鐘

         ·

        2022-07-26 07:05

        點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)??

        文章來(lái)源:https://c1n.cn/2jnRk


        目錄
        • 背景

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

        • 結(jié)語(yǔ)


        背景


        相信很多系統(tǒng)里都有這一種場(chǎng)景:用戶(hù)上傳 Excel,后端解析 Excel 生成相應(yīng)的數(shù)據(jù),校驗(yàn)數(shù)據(jù)并落庫(kù)。


        這就引發(fā)了一個(gè)問(wèn)題:如果 Excel 的行非常多,或者解析非常復(fù)雜,那么解析+校驗(yàn)的過(guò)程就非常耗時(shí)。


        如果接口是一個(gè)同步的接口,則非常容易出現(xiàn)接口超時(shí),進(jìn)而返回的校驗(yàn)錯(cuò)誤信息也無(wú)法展示給前端,這就需要從功能上解決這個(gè)問(wèn)題。


        一般來(lái)說(shuō)都是啟動(dòng)一個(gè)子線程去做解析工作,主線程正常返回,由子線程記錄上傳狀態(tài)+校驗(yàn)結(jié)果到數(shù)據(jù)庫(kù)。同時(shí)提供一個(gè)查詢(xún)頁(yè)面用于實(shí)時(shí)查詢(xún)上傳的狀態(tài)和校驗(yàn)信息。

        進(jìn)一步的,如果我們每一個(gè)上傳的任務(wù)都寫(xiě)一次線程池異步+日志記錄的代碼就顯得非常冗余。同時(shí),非業(yè)務(wù)代碼也侵入了業(yè)務(wù)代碼導(dǎo)致代碼可讀性下降。


        從通用性的角度上講,這種業(yè)務(wù)場(chǎng)景非常適合模板方法的設(shè)計(jì)模式。即設(shè)計(jì)一個(gè)抽象類(lèi),定義上傳的抽象方法,同時(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),
                  * 可以寫(xiě)入一個(gè)文件然后上傳到公司的文件服務(wù)器,
                  * 然后在查看結(jié)果的時(shí)候允許用戶(hù)下載該文件,
                  * 這里不展開(kāi)只做示意
                  */

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

              });
           }
        }


        如上文所示,模板方法的方式雖然能夠極大地減少重復(fù)代碼,但是仍有下面兩個(gè)問(wèn)題:

        • upload 方法得限定死參數(shù)結(jié)構(gòu),一旦有變化,不是很容易更改參數(shù)類(lèi)型 or 數(shù)量

        • 每個(gè)上傳的 service 還是要繼承一下這個(gè)抽象類(lèi),還是不夠簡(jiǎn)便和優(yōu)雅


        為解決上面兩個(gè)問(wèn)題,我也經(jīng)常進(jìn)行思考,結(jié)果在某次自定義事務(wù)提交 or 回滾的方法的時(shí)候得到了啟發(fā)。


        這個(gè)上傳的邏輯過(guò)程和事務(wù)提交的邏輯過(guò)程非常像,都是在實(shí)際操作前需要做初始化操作,然后在異?;蛘叱晒Φ臅r(shí)候做進(jìn)一步操作。


        這種完全可以通過(guò)環(huán)裝切面的方式實(shí)現(xiàn),由此,我寫(xiě)了一個(gè)小輪子給團(tuán)隊(duì)使用。(當(dāng)然了,這個(gè)小輪子在本人所在的大團(tuán)隊(duì)內(nèi)部使用的很好,但是不一定適合其他人,但是思路一樣,大家可以擴(kuò)展自己的功能)


        多說(shuō)無(wú)益,上代碼!


        代碼與實(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;
            //上傳類(lèi)型
            private String uploadType;
            //結(jié)束時(shí)間
            private Date endTime;
            // 開(kāi)始時(shí)間
            private Date startTime;
        }


        然后定義一個(gè)上傳的類(lèi)型枚舉,用于記錄是哪里操作的:
        public enum UploadType {
           未知(1,"未知"),
           類(lèi)型2(2,"類(lèi)型2"),
           類(lèi)型1(3,"類(lèi)型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 {
           // 記錄上傳類(lèi)型
           UploadType type() default UploadType.未知;
        }


        然后,編寫(xiě)切面:
        @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("-""");
              // 初始化一條上傳的日志,記錄開(kāi)始時(shí)間
              writeLogToDB(batchNo, type, new Date)
              // 線程池啟動(dòng)異步線程,開(kāi)始執(zhí)行上傳的邏輯,pjp.proceed()就是你實(shí)現(xiàn)的上傳功能
              uploadExecuteService.submit(() -> {
                 try {
                    String errorMessage = pjp.proceed();
                    // 沒(méi)有異常直接成功
                    if (StringUtils.isEmpty(errorMessage)) {
                        // 成功,寫(xiě)入數(shù)據(jù)庫(kù),具體不展開(kāi)了
                        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("-""");
              // 生成文件名稱(chēng)
              String fileName = "錯(cuò)誤日志_" +
                 DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH時(shí)mm分ss秒") + ExportConstant.txtSuffix;
              String filePath = "/home/xxx/xxx/" + fileName;
              // 生成一個(gè)文件,寫(xiě)入錯(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("寫(xiě)入文件錯(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ù),方便用戶(hù)查看錯(cuò)誤信息
              writeFailToDB(batchNo, s3Key, fileName);
              // 刪除文件,防止硬盤(pán)爆炸
              deleteFile(file)
           }

        }


        至此整個(gè)異步上傳功能就完成了,是不是很簡(jiǎn)單?(笑)


        那么怎么使用呢?更簡(jiǎn)單,只需要在 service 層加入注解即可,頂多就是把錯(cuò)誤信息 return 出去。
        @Upload(type = UploadType.類(lèi)型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é)語(yǔ)


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

        1. 面試難題:分布式 Session 實(shí)現(xiàn)難點(diǎn),這篇就夠!

        2. 合肥雖然沒(méi)有互聯(lián)網(wǎng)大廠,但有一些不錯(cuò)的企業(yè)和銀行!

        3. Docker 火了!外部網(wǎng)絡(luò)可直接訪問(wèn)映射到 127.0.0.1 的服務(wù)。。。

        4. Spring Event,賊好用的業(yè)務(wù)解耦神器!

        最近面試BAT,整理一份面試資料Java面試BATJ通關(guān)手冊(cè),覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。

        獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù) Java 領(lǐng)取,更多內(nèi)容陸續(xù)奉上。

        PS:因公眾號(hào)平臺(tái)更改了推送規(guī)則,如果不想錯(cuò)過(guò)內(nèi)容,記得讀完點(diǎn)一下在看,加個(gè)星標(biāo),這樣每次新文章推送才會(huì)第一時(shí)間出現(xiàn)在你的訂閱列表里。

        點(diǎn)“在看”支持小哈呀,謝謝啦??

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

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 天天插天天摸 | 成人网站毛片 | 干逼爽网| 自拍偷拍国内 | 中文字幕网av |