一、非同步導出Excel文件 1、設計思想 用戶無需在當前頁面等待導出結果,點擊導出按鈕後服務端即可返回前端提示用戶導出處理中請到下載中心查看結果。 具體業務文件導出實現由後臺非同步處理導出文件到騰訊COS存儲(有效期七天,到期自動刪除)。 用戶統一在下載中心菜單欄頁面中查看導出任務結果並下載文件。 2 ...
一、非同步導出Excel文件
1、設計思想
用戶無需在當前頁面等待導出結果,點擊導出按鈕後服務端即可返回前端提示用戶導出處理中請到下載中心查看結果。
具體業務文件導出實現由後臺非同步處理導出文件到騰訊COS存儲(有效期七天,到期自動刪除)。
用戶統一在下載中心菜單欄頁面中查看導出任務結果並下載文件。
2、技術組件
① EasyExcel 文檔地址:https://www.yuque.com/easyexcel/doc
② Redisson延遲隊列或xxl-job定時任務 (定時更新文件狀態為已過期)
③ 騰訊COS對象存儲
3、具體實現
① 導出文件記錄表
下載中心就是從這裡查數據下載文件
export_record
② 導出狀態枚舉 ExportStateEnum
public enum ExportStateEnum { FAILED(1,"失敗"), SUCCESS(2,"成功"), GOING(3,"進行中"), EXPIRED(4,"已過期"), ; private Integer value; private String msg; }
③ 非同步導出工具類 (PS:有待優化)
@Slf4j @Component public class AsyncExcelUtil { @Autowired ExportRecordFeignClient exportRecordFeignClient; @Autowired COSClient cosClient; @Autowired ThreadPoolTaskExecutor taskExecutor; Long recordId; public ResponseData asyncExport(UserInfo userInfo, String fileName, Runnable r) { //1、資料庫初始化操作記錄 ResponseData<Long> initResult = this.exportRecordInit(userInfo, fileName); if (Objects.nonNull(initResult) && Objects.equals(initResult.getCode(), ResponseEnum.SUCCESS.getCode())) { this.recordId = initResult.getData(); taskExecutor.execute(r); return ResponseData.success("操作成功"); } return ResponseData.fail("操作失敗"); } /** * 查詢當前用戶下導出文件記錄數據 * * @param entity */ public ResponseData<List<ExportRecordEntity>> queryExportRecordList(ExportRecordEntity entity) { return exportRecordFeignClient.queryExportRecordList(entity); } /** * 初始化導入導出記錄表 * * @param userInfo * @param fileName */ public ResponseData<Long> exportRecordInit(UserInfo userInfo, String fileName) { //1、資料庫初始化操作記錄 ExportRecordEntity exportRecordEntity = new ExportRecordEntity(); exportRecordEntity.setTenantId(Long.parseLong(userInfo.getUniversityId())); exportRecordEntity.setOpType(1); exportRecordEntity.setProgress(30); exportRecordEntity.setIsSuccess(2); exportRecordEntity.setExportFileName(fileName); exportRecordEntity.setCreatedId(Long.parseLong(userInfo.getEmployeeId())); exportRecordEntity.setCreatedName(userInfo.getUserName()); exportRecordEntity.setCreatedTime(LocalDateTime.now()); exportRecordEntity.setUpdatedTime(exportRecordEntity.getCreatedTime()); return exportRecordFeignClient.exportInit(exportRecordEntity); } /** * 數據整理完畢更新進度 * * @param recordId */ public boolean exportDataComplete(Long recordId) { ExportRecordEntity exportRecordEntity = new ExportRecordEntity(); exportRecordEntity.setId(recordId); exportRecordEntity.setProgress(80); exportRecordEntity.setUpdatedTime(LocalDateTime.now()); return exportRecordFeignClient.exportDataComplete(exportRecordEntity); } /** * 導出excel文件上傳到騰訊COS並更新導出操作結果 * * @param data 數據列表 * @param fileNm 文件名 * @param sheetNm excel文件sheet名稱 * @param <T> */ public <T> boolean exportToCos(List<T> data, String fileNm, String sheetNm) { Either<String, String> exportResult = asyncExport1(recordId, data, fileNm, sheetNm); ExportRecordEntity exportRecordEntity = new ExportRecordEntity(); exportRecordEntity.setId(recordId); exportRecordEntity.setUpdatedTime(LocalDateTime.now()); if (exportResult.isLeft()) { exportRecordEntity.setIsSuccess(0); exportRecordEntity.setFailReason(exportResult.getLeft()); } else { exportRecordEntity.setProgress(100); exportRecordEntity.setIsSuccess(1); exportRecordEntity.setExportFileUrl(exportResult.get()); } return exportRecordFeignClient.exportSaveResult(exportRecordEntity); } /** * 導出excel文件上傳到騰訊COS * * @param data 數據列表 * @param fileNm 文件名 * @param sheetNm excel文件sheet名稱 * @param <T> */ public <T> Either<String, String> asyncExport1(Long recordId, List<T> data, String fileNm, String sheetNm) { if (Objects.isNull(data) || CollectionUtils.isEmpty(data)) { return Either.left("數據為空"); } else { this.exportDataComplete(recordId); } String filePath = ""; try { //導出操作 String basePath = ResourceUtils.getURL("classpath:").getPath() + "static/"; // 建立新的文件 File fileExist = new File(basePath); // 文件夾不存在,則新建 if (!fileExist.exists()) { fileExist.mkdirs(); } String fileName = fileNm + "-" + System.currentTimeMillis() + ".xlsx"; filePath = basePath + fileName; EasyExcel.write(filePath, data.get(0).getClass()).sheet(sheetNm).doWrite(data); // 指定要上傳的文件 File localFile = new File(filePath); // 指定文件將要存放的存儲桶 String bucketName = Constants.DOCUMENT_BUCKET; // 指定文件上傳到 COS 上的路徑,即對象鍵。例如對象鍵為folder/picture.jpg,則表示將文件 picture.jpg 上傳到 folder 路徑下 String key = "temp/asyncexcel/" + fileName; PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, localFile); PutObjectResult putObjectResult = cosClient.putObject(putObjectRequest); return Either.right(cosClient.getObjectUrl(bucketName, key).toString()); } catch (Exception e) { log.error("非同步導出excel異常:", e); return Either.left(e.getMessage()); } finally { removeTempFile(filePath); } } /***************************** private私有方法 *********************************/ private static void removeTempFile(String filePath) { File delFile = new File(filePath); if (delFile.exists()) { delFile.delete(); } } }
4、AsyncExcelUtil工具類具體使用示例
@PostMapping(value = "/testAsyncExport", produces = {"application/json"}) public ResponseData testAsyncExport(@RequestBody CommonRequestParam param) { UserInfo userInfo = param.getUserInfo(); String fileName = "非同步導出測試文件"; UserInfoParam userParam = new UserInfoParam(); userParam.setUserName(userInfo.getUserName()); userParam.setUserId(userInfo.getUserId()); userParam.setModule("各自業務模塊名稱,如:直播數據"); return asyncExcelUtil.asyncExport(userParam, fileName, () -> { //模擬封裝得到要導出的數據 List<ExportVo> retVo = new ArrayList<>(); for (int i = 0; i < 7000; i++) { ExportVo vo = new ExportVo(); vo.setModule("商城"); vo.setName("張三" + i); vo.setUserDept("部門" + i); vo.setWatchTotal("20"); retVo.add(vo); } asyncExcelUtil.exportToCos(userParam, retVo, fileName, "直播測試數據"); }); }
參數說明
① userParam 記錄操作人信息
② fileName 文件名稱
③ 行為參數化 不需要每個業務再創建對應的實現類,Lamda表達式代替內名內部類實現靈活。Runnable 接收傳入線程池非同步執行。
5、優化點:子線程異常處理
由於上面的實現如果非同步子線程中發生異常是會直接退出的,無法記錄任何日誌,如果讓業務方自己添加try catch模塊有可能會造成疏漏而且也不方便。
優化方案:為線程設置“未捕獲異常處理器”UncaughtExceptionHandler
在子線程中多添加一行固定代碼設置當前線程的異常處理器:
Thread.currentThread().setUncaughtExceptionHandler(new CustomThreadExceptionHandler());
public class CustomThreadExceptionHandler implements Thread.UncaughtExceptionHandler{ @Override public void uncaughtException(Thread t,Throwable e) { //處理 記錄到庫數據獲取異常 } }
6、非同步導入Excel方案
實現思路整合和非同步導出一致,在下載中心列表中區分導入和導出的操作,並且導入操作須記錄能夠直接跳轉到對應業務菜單頁面去,能夠下載導入錯誤數據的excel文件。