一、异步导出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文件。