<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)異步上傳

          共 13392字,需瀏覽 27分鐘

           ·

          2022-07-23 15:10

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

          前言

          相信很多系統(tǒng)里都有這一種場景:用戶上傳Excel,后端解析Excel生成相應的數(shù)據(jù),校驗數(shù)據(jù)并落庫。這就引發(fā)了一個問題:如果Excel的行非常多,或者解析非常復雜,那么解析+校驗的過程就非常耗時。如果接口是一個同步的接口,則非常容易出現(xiàn)接口超時,進而返回的校驗錯誤信息也無法展示給前端,這就需要從功能上解決這個問題。一般來說都是啟動一個子線程去做解析工作,主線程正常返回,由子線程記錄上傳狀態(tài)+校驗結果到數(shù)據(jù)庫。同時提供一個查詢頁面用于實時查詢上傳的狀態(tài)和校驗信息。
          、多線程處理導入excel
          進一步的,如果我們每一個上傳的任務都寫一次線程池異步+日志記錄的代碼就顯得非常冗余。同時,非業(yè)務代碼也侵入了業(yè)務代碼導致代碼可讀性下降。從通用性的角度上講,這種業(yè)務場景非常適合模板方法的設計模式。即設計一個抽象類,定義上傳的抽象方法,同時實現(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, "導入中");
                   // 一個字符串,用于記錄upload的校驗信息
                   String errorLog = "";
                   //執(zhí)行上傳
                   try {
                      errorLog = upload(data);
                      writeSuccess(uuid, "導入中", updateTime);
                   } catch (Exception e) {
                      LOGGER.error("導入錯誤", e);
                      //計入導入錯誤日志
                      writeFailToDb(uuid, "導入失敗", e.getMessage(), updateTime);
                   }
                   /**
                    * 檢查一下upload是不是返回了錯誤日志,如果有,需要注意記錄
                    *
                    * 因為錯誤日志可能比較長,
                    * 可以寫入一個文件然后上傳到公司的文件服務器,
                    * 然后在查看結果的時候允許用戶下載該文件,
                    * 這里不展開只做示意
                    */

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

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

          代碼與實現(xiàn)

          首先定義一個日志實體
          public class FileUploadLog {
             private Integer id;
              // 唯一編碼
              private String batchNo;
              // 上傳到文件服務器的文件key
              private String key;
              // 錯誤日志文件名
              private String fileName;
              //上傳狀態(tài)
              private Integer status;
              //上傳人
              private String createName;
              //上傳類型
              private String uploadType;
              //結束時間
              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);
             }
          }
          最后,定義一個注解,用于標識切點
          @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) {
                 // 獲取方法上的注解,進而獲取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("導入失敗:", 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("關閉錯誤", e);
                   }
                }
                // 上傳錯誤日志文件到文件服務器,我們用的是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);
          }

          結語

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

          最近熱文閱讀:

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

          瀏覽 34
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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| 天天干天天做 | 日韩精品成人无码 |