<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          實現(xiàn)一個小輪子,用AOP實現(xiàn)異步上傳

          共 13108字,需瀏覽 27分鐘

           ·

          2022-07-11 20:37

          你知道的越多,不知道的就越多,業(yè)余的像一棵小草!

          你來,我們一起精進(jìn)!你不來,我和你的競爭對手一起精進(jìn)!

          編輯:業(yè)余草

          juejin.cn/post/7102343528525037576

          推薦:https://www.xttblog.com/?p=5351

          前言

          相信很多系統(tǒng)里都有這一種場景:用戶上傳Excel,后端解析Excel生成相應(yīng)的數(shù)據(jù),校驗數(shù)據(jù)并落庫。這就引發(fā)了一個問題:如果Excel的行非常多,或者解析非常復(fù)雜,那么解析+校驗的過程就非常耗時。如果接口是一個同步的接口,則非常容易出現(xiàn)接口超時,進(jìn)而返回的校驗錯誤信息也無法展示給前端,這就需要從功能上解決這個問題。一般來說都是啟動一個子線程去做解析工作,主線程正常返回,由子線程記錄上傳狀態(tài)+校驗結(jié)果到數(shù)據(jù)庫。同時提供一個查詢頁面用于實時查詢上傳的狀態(tài)和校驗信息。

          多線程處理導(dǎo)入excel

          進(jìn)一步的,如果我們每一個上傳的任務(wù)都寫一次線程池異步+日志記錄的代碼就顯得非常冗余。同時,非業(yè)務(wù)代碼也侵入了業(yè)務(wù)代碼導(dǎo)致代碼可讀性下降。從通用性的角度上講,這種業(yè)務(wù)場景非常適合模板方法的設(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) {
                // 生成一個唯一編號
                String uuid = UUID.randomUUID().toString().replace("-""");
                uploadExecuteService.submit(() -> {
                   // 記錄日志
                   writeLogToDb(uuid, userName, updateTime, "導(dǎo)入中");
                   // 一個字符串,用于記錄upload的校驗信息
                   String errorLog = "";
                   //執(zhí)行上傳
                   try {
                      errorLog = upload(data);
                      writeSuccess(uuid, "導(dǎo)入中", updateTime);
                   } catch (Exception e) {
                      LOGGER.error("導(dǎo)入錯誤", e);
                      //計入導(dǎo)入錯誤日志
                      writeFailToDb(uuid, "導(dǎo)入失敗", e.getMessage(), updateTime);
                   }
                   /**
                    * 檢查一下upload是不是返回了錯誤日志,如果有,需要注意記錄
                    *
                    * 因為錯誤日志可能比較長,
                    * 可以寫入一個文件然后上傳到公司的文件服務(wù)器,
                    * 然后在查看結(jié)果的時候允許用戶下載該文件,
                    * 這里不展開只做示意
                    */

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

                });
             }
          }

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

          • upload方法得限定死參數(shù)結(jié)構(gòu),一旦有變化,不是很容易更改參數(shù)類型or數(shù)量
          • 每個上傳的service還是要繼承一下這個抽象類,還是不夠簡便和優(yōu)雅

          為解決上面兩個問題,我也經(jīng)常進(jìn)行思考,結(jié)果在某次自定義事務(wù)提交or回滾的方法的時候得到了啟發(fā)。這個上傳的邏輯過程和事務(wù)提交的邏輯過程非常像,都是在實際操作前需要做初始化操作,然后在異?;蛘叱晒Φ臅r候做進(jìn)一步操作。這種完全可以通過環(huán)裝切面的方式實現(xiàn),由此,我寫了一個小輪子給團(tuán)隊使用。(當(dāng)然了,這個小輪子在本人所在的大團(tuán)隊內(nèi)部使用的很好,但是不一定適合其他人,但是思路一樣,大家可以擴(kuò)展自己的功能)

          「多說無益,上代碼!」

          代碼與實現(xiàn)

          首先定義一個日志實體

          public class FileUploadLog {
             private Integer id;
              // 唯一編碼
              private String batchNo;
              // 上傳到文件服務(wù)器的文件key
              private String key;
              // 錯誤日志文件名
              private String fileName;
              //上傳狀態(tài)
              private Integer status;
              //上傳人
              private String createName;
              //上傳類型
              private String uploadType;
              //結(jié)束時間
              private Date endTime;
              // 開始時間
              private Date startTime;
          }

          然后定義一個上傳的類型枚舉,用于記錄是哪里操作的

          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);
             }
          }

          最后,定義一個注解,用于標(biāo)識切點

          @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("-""");
                // 初始化一條上傳的日志,記錄開始時間
                writeLogToDB(batchNo, type, new Date)
                // 線程池啟動異步線程,開始執(zhí)行上傳的邏輯,pjp.proceed()就是你實現(xiàn)的上傳功能
                uploadExecuteService.submit(() -> {
                   try {
                      String errorMessage = pjp.proceed();
                      // 沒有異常直接成功
                      if (StringUtils.isEmpty(errorMessage)) {
                          // 成功,寫入數(shù)據(jù)庫,具體不展開了
                          writeSuccessToDB(batchNo);
                      } else {
                          // 失敗,因為返回了校驗信息
                          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) {
                 // 生成上傳錯誤日志文件的文件key
                String s3Key = UUID.randomUUID().toString().replace("-""");
                // 生成文件名稱
                String fileName = "錯誤日志_" +
                   DateUtil.dateToString(new Date(), "yyyy年MM月dd日HH時mm分ss秒") + ExportConstant.txtSuffix;
                String filePath = "/home/xxx/xxx/" + fileName;
                // 生成一個文件,寫入錯誤數(shù)據(jù)
                File file = new File(filePath);
                OutputStream outputStream = null;
                try {
                   outputStream = new FileOutputStream(file);
                   outputStream.write(message.getBytes());

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

          }

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

          那么怎么使用呢?更簡單,只需要在service層加入注解即可,頂多就是把錯誤信息return出去。

          @Upload(type = UploadType.類型1)
          public String upload(List<ClassOne> items)  {
             if (items == null || items.size() == 0) {
                return;
             }
             //校驗
             String error = uploadCheck(items);
             if (StringUtils.isNotEmpty) {
                 return error;
             }
             //刪除舊的
             deleteAll();
             //插入新的
             batchInsert(items);
          }

          結(jié)語

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

          瀏覽 64
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  最新黄色视频 | 性欧美性A | 毛茸茸毛片 | 爱爱免费视频网址 | 99爱色 |