背景
业务中很常见的场景,就是下载. 而随着业务数据越来越大, 下载的负担也越来越重, 时间越来越久
因此经常会将其做成异步的, 先给前端返回,然后开一个线程去处理. 然后等处理完用户到一个专门的页面下载.
表
要实现这样的功能, 肯定要把下载的内容存起来:
-- t_train_file_center definition
CREATE 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
*/
@Override
public 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();
}
@Override
public 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);
}
}
//然后由业务来提供输出流
//比如excel
ByteArrayOutputStream outputStream = (ByteArrayOutputStream) excelWriter.getOutputStream();