实现一个复用性比较强的异步导出表下载功能,大致思想如下:
- 异步任务处理:
- 基于Spring的TaskExecutor的异步任务处理机制,系统异步生成报表数据,避免阻塞主线程影响系统性能。
- 事件驱动架构:
- 使用ApplicationEventPublisher发布文件上传事件,通知文件下载中心模块进行文件上传,确保事件传递的可靠性和一致性。
- 设计了幂等性处理逻辑,防止重复事件导致的数据不一致问题。
代码框架如下:
public Long export(Req req) {
return export(req, bizCode, () -> doExport(req));
}
private Long export(Req req, String bizCode, Runnable exportFunction) {
// 任务IDzz:全局唯一编码
Long taskId = "";
// 设置任务ID到请求对象中
req.setTaskId(taskId);
// 异步执行导出:TaskExecutor
taskExecutor.execute(exportFunction);
// 直接返回任务ID
return taskId;
}
private void doExport(Req req) {
List<DO> data = Lists.newArrayList();
String fileName = req.getFileName();
// 使用分批次查询,避免一次性把大量数据放到内存里
long currentMinId = 0L;
long batchSize = 1000L;
try {
for (; ; ) {
list = mapper.selectList;
//没查到任何数据直接结束
if (list.isEmpty()) {
break;
}
data.addAll(list);
//查询到最后一页则跳出
if (list.size() < batchSize) {
break;
}
//下次的筛序起始id
currentMinId = list.get(list.size() - 1).getId();
}
List<ExcelTemplate> excelDataList = data.stream().map(Mapping.INSTANCE::convertToExcel).toList();
// 执行文件导出+上传任务
taskExecutor.execute(
new ExportTask<>(req.getTaskId(),
fileName,
com.google.common.collect.Lists.newArrayList(
new ExcelExportConfig<>(
0,
null,
ExcelTemplate.class,
excelDataList.stream().toList())),
publisher) //ApplicationEventPublisher
);
} catch (Exception e) {
log.error("[doExport] method error:{}", e.getMessage(), e);
}
}
public class ExportTask<C, D> implements Runnable {
private static final Logger log = LoggerFactory.getLogger(ExportTask.class);
private final Long taskId;
private final String fileName;
private final List<ExcelExportConfig<C, D>> configs;
private final ApplicationEventPublisher publisher;
public ExportTask(Long taskId, String fileName, List<ExcelExportConfig<C, D>> configs, ApplicationEventPublisher publisher) {
this.taskId = taskId;
this.fileName = fileName;
this.publisher = publisher;
this.configs = configs;
}
@Override
public void run() {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
try {
//写入数据
configs.forEach(
(config) -> {
// EasyExcel.write(filePath, clazz).sheet(sheetNum, sheetName).doWrite(data);
WriteSheet writeSheet = EasyExcel.writerSheet(config.getNum(), config.getSheetName()).head(config.getClazz()).build();
excelWriter.write(CollectionUtils.emptyIfNull(config.getData()), writeSheet);
}
);
excelWriter.finish();
//mock文件
MockMultipartFile mockMultipartFile = new MockMultipartFile(fileName, fileName, null, outputStream.toByteArray());
//发布文件上传事件
publisher.publishEvent(new FileUploadEvent(this, taskId, mockMultipartFile));
} catch (Exception e) {
log.error("excel导出任务:导出文件:{},时出现异常原因:{}", fileName, Throwables.getStackTraceAsString(e));
throw new RuntimeException(e);
} finally {
//关闭流
excelWriter.finish();
try {
outputStream.close();
} catch (Exception e) {
log.error("导出任务ExportTask关闭outputStream失败!");
}
}
}
}
public class ExcelExportConfig<C, D> {
/**
* num
*/
private int num;
/**
* sheet名
*/
private String sheetName;
/**
* 模板class
*/
private Class<C> clazz;
/**
* 数据集
*/
private List<D> data;
public ExcelExportConfig(int num, String sheetName, Class<C> clazz, List<D> data) {
this.num = num;
this.sheetName = sheetName;
this.clazz = clazz;
this.data = data;
}
}
@Getter
public class FileUploadEvent extends ApplicationEvent implements Serializable {
private MultipartFile multipartFile;
private Long taskId;
public FileUploadEvent(Object source) {
super(source);
}
public FileUploadEvent(Object source, MultipartFile multipartFile) {
super(source);
this.multipartFile = multipartFile;
}
public FileUploadEvent(Object source, Long taskId, MultipartFile multipartFile) {
super(source);
this.multipartFile = multipartFile;
this.taskId = taskId;
}
}
@Slf4j
@Component
public class FileUploadListener implements ApplicationListener<FileUploadEvent> {
@Override
public void onApplicationEvent(@NotNull FileUploadEvent event) {
log.info("监听到上传事件taskId:{},文件名:{},文件大小:{}kb", event.getTaskId(), event.getMultipartFile().getName(), event.getMultipartFile().getSize() / 1024);
Long taskId = event.getTaskId();
InputStream inputStream = null;
Stopwatch stopwatch = Stopwatch.createStarted(Ticker.systemTicker());
try {
MultipartFile multipartFile = event.getMultipartFile();
//文件名+后缀(.xlsx)
String fileName = multipartFile.getName();
//文件输入流
inputStream = multipartFile.getInputStream();
//上传OSS
log.info("开始上传文件taskId:{},文件名:{},文件大小:{}kb", event.getTaskId(), event.getMultipartFile().getName(), event.getMultipartFile().getSize() / 1024);
//oss上传对象文件名
String objectName = LocalDate.now().toString().concat(StrPool.SLASH).concat(IdUtil.fastSimpleUUID()).concat(StrPool.DOT).concat(fileName.substring(fileName.lastIndexOf(StrPool.DOT)));
ossClient.upload(inputStream, objectName, fileName);
//获取到oss文件链接
String url = ossClient.generateUrl(objectName, Duration.ofHours(1));
log.info("上传文件结束taskId:{},文件名:{},总共耗时:{}ms", event.getTaskId(), event.getMultipartFile().getName(), stopwatch.elapsed(TimeUnit.MILLISECONDS));
} catch (Exception e) {
log.error("文件上传OSS异常, taskId={}", taskId, e);
} finally {
//关流
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
log.error("文件上传关闭文件流异常", e);
}
}
}
}

3570

被折叠的 条评论
为什么被折叠?



