异步实现excel导出与下载,提高可扩展性

实现一个复用性比较强的异步导出表下载功能,大致思想如下:

  1. 异步任务处理:
  • 基于Spring的TaskExecutor的异步任务处理机制,系统异步生成报表数据,避免阻塞主线程影响系统性能。
  1. 事件驱动架构:
  • 使用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);
            }
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值