背景
业务中很常见的场景,就是下载. 而随着业务数据越来越大, 下载的负担也越来越重, 时间越来越久
因此经常会将其做成异步的, 先给前端返回,然后开一个线程去处理. 然后等处理完用户到一个专门的页面下载.
表
要实现这样的功能, 肯定要把下载的内容存起来:
-- t_train_file_center definitionCREATE TABLE `t_file_center` (`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`file_name` varchar(200) NOT NULL DEFAULT '' COMMENT '文件名',`create_id` varchar(20) NOT NULL DEFAULT '' COMMENT '创建人',`create_name` varchar(50) NOT NULL DEFAULT '' COMMENT '创建人名字',`status` int(11) NOT NULL DEFAULT '0' COMMENT '状态. 1:进行中, 2:已完成, 3: 已失效, 4:处理失败, -1: 已取消, -2:已删除',`file_path` varchar(500) NOT NULL DEFAULT '' COMMENT '文件路径',`is_delete` int(11) NOT NULL DEFAULT '0' COMMENT '是否物理删除. 1: 是',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',`update_id` varchar(30) NOT NULL DEFAULT '' COMMENT '更新人',`update_name` varchar(50) NOT NULL DEFAULT '' COMMENT '更新人名字',`remark` varchar(1000) DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`),KEY `file_name_IDX` (`file_name`) USING BTREE,KEY `create_id_IDX` (`create_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='批量文件导出中心';
如果是要实现比如批量压缩后打成压缩包的,还需要一个详情表来记录压缩包里的文件信息
思路
思路大概就是:
- 先初始化记录,状态标记为进行中
- 返回前端基本信息
- 异步处理
- 更新结果
- 定期删除过期文件(可选)
代码
/*** 处理文件创建,下载, 直接写入服务器版本* */public String doSaveFile(String filename, Consumer<OutputStream> consumer, ExecutorService executorService) throws IOException {String path = "filecenter/tempfile" + FilesUtils.generateDirPathByTimestamp() + "/" + filename;//获取当前登录人SessionUser user = UserCtxUtil.getCurrentUser();//创建文件,我这个是直接在nas上创建.File file = fileCommonService.createFile(path);//创建文件结束//记录文件path,写入表中Long id = this.saveTempFile(filename, path, Status.PROCESSING, user);if (executorService != null) {//开始处理,如果提供了线程池,异步处理executorService.execute(() -> process(consumer, file, id, user));} else {//同步处理process(consumer, file, id, user);}return path;}/*** 记录到文件中心,默认7天后删除文件** @param path* @return*/@Overridepublic Long saveTempFile(String filename, String path, FileCenterEnum.Status status, SessionUser user) {FileCenter fileCenter = new FileCenter();fileCenter.setFileName(filename);fileCenter.setFilePath(path);fileCenter.setStatus(status.getCode());if (user != null) {fileCenter.setCreateId(user.getEmpNumber());fileCenter.setCreateName(user.getName());fileCenter.setCreateTime(new Date());fileCenter.setUpdateId(user.getEmpNumber());fileCenter.setUpdateName(user.getName());}super.save(fileCenter);return fileCenter.getId();}@Overridepublic void updateStatus(Long id, FileCenterEnum.Status status, String remark, SessionUser user) {FileCenter fileCenter = new FileCenter();fileCenter.setId(id);fileCenter.setStatus(status.getCode());fileCenter.setRemarkremark,1000);if (user != null) {fileCenter.setUpdateId(user.getEmpNumber());fileCenter.setUpdateName(user.getName());fileCenter.setUpdateTime(new Date());}super.updateById(fileCenter);}/*** 处理文件流的写入*/private void process(Consumer<OutputStream> consumer, File file, Long id, SessionUser user) {try (OutputStream os = Files.newOutputStream(file.toPath());) {log.info("开始处理文件....");consumer.accept(os);log.info("处理文件结束....");this.updateStatus(id, Status.PROCESS_DONE, null, user);} catch (Exception e) {log.error("写入文件失败:", e);this.updateStatus(id, Status.PROCESS_FAILED, "写入失败:" + ExceptionUtil.getMessage(e), user);}}/*** (生成文件名用),生成时间戳格式的目录名* @return String*/public static String generateDirPathByTimestamp() {StringBuilder sb = new StringBuilder();Date date = new Date();Long stamp = date.getTime();String formatStamp = String.format("%015d", stamp);for (int i = 0; i < formatStamp.length() / 3; i++) {sb.append("/").append(formatStamp.substring(3 * i, 3 * (i + 1)));}return sb.toString();}
文件状态枚举
//文件状态枚举public enum Status{/****/DEFAULT(0,"未知"),PROCESSING(1,"进行中"),PROCESS_DONE(2,"已完成"),INVALID(3,"已失效"),PROCESS_FAILED(4,"处理失败"),CANCELED(-1,"已取消"),DELETE(-2,"已删除"),;private int code;private String msg;Status(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}public static Status of(int code){for (Status value : values()) {if (value.getCode() == code){return value;}}return null;}}
使用示例
//收到请求List<List<String>> head;List<List<String>> data;ExecutorService threadPool;doSaveFile("文件导出.xlsx",(os->{EasyExcel.write(os).head(head).doWrite(data);}),threadPool)//返回相应return Result;
其他方式
以上是nas或者直接写入服务器磁盘的方式. 如果是要上传到oss,要么可以先创建在服务器本地,写入流后,再上传到oss然后更新状态为成功. 或者process方法里直接采用oss提供的方式写入
那这样process可以考虑换成Supplier:
private void process(Supplier<ByteArrayOutputStream> supplier, File file, Long id, SessionUser user) {try (ByteArrayOutputStream os = supplier.get()) {log.info("开始处理文件....");//输出流写入oss输入流InputStream inputStream = new ByteArrayInputStream(os.toByteArray());log.info("处理文件结束....");this.updateStatus(id, Status.PROCESS_DONE, null, user);} catch (Exception e) {log.error("写入文件失败:", e);this.updateStatus(id, Status.PROCESS_FAILED, "写入失败:" + ExceptionUtil.getMessage(e), user);}}//然后由业务来提供输出流//比如excelByteArrayOutputStream outputStream = (ByteArrayOutputStream) excelWriter.getOutputStream();