使用Spring AOP實(shí)現(xiàn)異步文件上傳
程序員的成長(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)信息。

進(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>
推薦閱讀:
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)??!
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??朕已閱?
![]()
