<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>

          使用Spring AOP實(shí)現(xiàn)異步文件上傳

          共 7038字,需瀏覽 15分鐘

           ·

          2022-09-06 09:01

          ce05e03365ed82a7053dc33e49d1e3be.webp程序員的成長(zhǎng)之路互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享? 關(guān)注


          閱讀本文大概需要 5 分鐘。

          來(lái)自:https://c1n.cn/2jnRk

          背景

          相信很多系統(tǒng)里都有這一種場(chǎng)景:用戶上傳 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è)查詢頁(yè)面用于實(shí)時(shí)查詢上傳的狀態(tài)和校驗(yàn)信息。

          93997b91685cba99f1cbfa0672cca838.webp



          進(jìn)一步的,如果我們每一個(gè)上傳的任務(wù)都寫(xiě)一次線程池異步+日志記錄的代碼就顯得非常冗余。同時(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(10,?20,?300L,
          ??????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í)候允許用戶下載該文件,
          ??????????*?這里不展開(kāi)只做示意
          ??????????*/

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

          ??????});
          ???}
          }

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

          • upload 方法得限定死參數(shù)結(jié)構(gòu),一旦有變化,不是很容易更改參數(shù)類型 or 數(shù)量
          • 每個(gè)上傳的 service 還是要繼承一下這個(gè)抽象類,還是不夠簡(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;
          ????//上傳類型
          ????private?String?uploadType;
          ????//結(jié)束時(shí)間
          ????private?Date?endTime;
          ????//?開(kāi)始時(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.未知;
          }

          然后,編寫(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(10,?20,?300L,
          ??????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("-",?"");
          ??????//?生成文件名稱
          ??????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ù),方便用戶查看錯(cuò)誤信息
          ??????writeFailToDB(batchNo,?s3Key,?fileName);
          ??????//?刪除文件,防止硬盤(pán)爆炸
          ??????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é)語(yǔ)

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

          <END>

          推薦閱讀:

          成都核酸檢測(cè)系統(tǒng)崩潰,東軟背鍋 ?

          SpringCloud 三種服務(wù)調(diào)用方式,你學(xué)會(huì)了嗎?

              

          互聯(lián)網(wǎng)初中高級(jí)大廠面試題(9個(gè)G)

          內(nèi)容包含Java基礎(chǔ)、JavaWeb、MySQL性能優(yōu)化、JVM、鎖、百萬(wàn)并發(fā)、消息隊(duì)列、高性能緩存、反射、Spring全家桶原理、微服務(wù)、Zookeeper......等技術(shù)棧!

          ?戳閱讀原文領(lǐng)??! ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??朕已閱? 46c4ed1930cefb2b77165284ddaf70ce.webp

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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  影音先锋一区二区三区视频特色 | 国产草比视频 | 又粗又长又大的黄视频 | 免费无码性爱视频 | 猫咪www成人免费网站无码 |