数据分页异步后台导出excel

数据分页异步后台导出excel

异步导出设计

下面根据如上设计列出具体方案(没有文件服务器,直接从后台服务下载)

表设计

复制代码
DROP TABLE IF EXISTS `task`;

CREATE TABLE `task` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `type` varchar(2) NOT NULL DEFAULT '1' COMMENT '1:导出',
  `status` varchar(100) DEFAULT NULL COMMENT '状态',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `start_time` datetime DEFAULT NULL,
  `end_time` datetime DEFAULT NULL,
  `err_msg` text,
  `url` varchar(1000) DEFAULT NULL COMMENT '文件地址',
  `progress` decimal(3,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=107 DEFAULT CHARSET=utf8;

异步导出类

Java 复制代码
package com.async_export_demo.export;

import com.async_export_demo.config.MyExportConfig;
import com.async_export_demo.model.Task;
import com.async_export_demo.service.TaskService;
import com.async_export_demo.util.SpringContextUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Getter;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

@Component
@Log
public class AsyncExportService {

    private static final ThreadPoolTaskExecutor EXECUTOR = new ThreadPoolTaskExecutor();

    /**
     * 进度,保留两位小数
     */
    private static MathContext MATH_CONTEXT = new MathContext(2, RoundingMode.HALF_UP);

    static {
        //设置核心线程数
        EXECUTOR.setCorePoolSize(2);
        //设置最大线程数
        EXECUTOR.setMaxPoolSize(4);
        //设置线程被回收的空闲时长
        EXECUTOR.setKeepAliveSeconds(6);
        //设置队列容量
        EXECUTOR.setQueueCapacity(2);
        //设置线程前缀
        EXECUTOR.setThreadNamePrefix("export-");
        //设置拒绝策略
        EXECUTOR.setRejectedExecutionHandler(new AbortPolicy());
        //初始化线程池
        EXECUTOR.initialize();
    }

    @Autowired
    private TaskService taskService;

    @Autowired
    private Map<ExportEnum, DataProvider> dataProviderMap;

    @Autowired
    private MyExportConfig myExportConfig;

    // 导出配置类
    public static class ExportConfig {
        private int pageSize = 1000;  // 每页数据量
        private int maxRetry = 3;     // 失败重试次数
        private String fileType = "xlsx"; // 文件类型(csv/xlsx)
        private boolean compress = true; // 是否压缩
    }

    public static class ExportContext {
        private final Map<String, Object> params = new HashMap<>();

        public void put(String key, Object value) {
            params.put(key, value);
        }

        public Object get(String key) {
            return params.get(key);
        }
    }

    /**
     * 因为我想让线程池执行拒绝策略时可以拿到 taskId,以便可以更新状态,所以新定义了一个Runnable
     */
    public static class ExportRunnable implements Runnable {

        @Getter
        private final BigInteger taskId;

        private final Runnable runnable;

        public ExportRunnable(BigInteger taskId, Runnable runnable) {
            this.taskId = taskId;
            this.runnable = runnable;
        }

        @Override
        public void run() {
            runnable.run();
        }

    }

    /**
     * 线程池拒绝策略
     */
    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() {
        }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            RejectedExecutionException ex = new RejectedExecutionException("Task " + r.toString() +
                    " rejected from " +
                    e.toString());
            if (r instanceof ExportRunnable) {
                ExportRunnable r2 = (ExportRunnable) r;
                Task entity = new Task();
                entity.setId(r2.getTaskId());
                entity.setStatus("FAILED");
                entity.setErrMsg(ex.getMessage());
                TaskService taskService1 = SpringContextUtil.getApplicationContext().getBean(TaskService.class);
                taskService1.updateExportTask(entity);
            }
            throw ex;
        }
    }

    public BigInteger asyncExport(ExportEnum exportEnum) {
        // 超时时长毫秒
        long timeout = myExportConfig.getTimeoutUnit().toMillis(myExportConfig.getTimeout());
        DataProcessor dataProcessor = new DefaultDataProcessor(myExportConfig, exportEnum.getClz());
        return asyncExport(null, dataProviderMap.get(exportEnum), dataProcessor, timeout);
    }

    public BigInteger asyncExport(ExportConfig config, DataProvider<?> dataProvider, DataProcessor dataProcessor, long timeout) {
        BigInteger taskId = taskService.addExportTask();
        EXECUTOR.execute(new ExportRunnable(taskId, () -> {
            try {
                updateTaskStart(taskId);
                executeExport(taskId, dataProvider, dataProcessor, timeout);
                updateTaskEnd(taskId, dataProcessor.getUrl());
            } catch (Exception e) {
                updateTaskFail(taskId, e.getMessage());
            }
        }));

        /*try {
            updateTaskStart(taskId);
            executeExport(taskId, dataProvider, dataProcessor);
            updateTaskEnd(taskId);
        } catch (Exception e) {
            updateTaskFail(taskId, e.getMessage());
        }*/

        return taskId;
    }

    private <T> void executeExport(BigInteger taskId, DataProvider<T> dataProvider, DataProcessor dataProcessor, long timeout) {
        long start = System.currentTimeMillis();
        long currentPage = 1;
        Page<T> page = null;
        // 新建上下文对象,如需传递参数可以放在里面
        ExportContext context = new ExportContext();
        do {
            // todo 有没其他方法判断是否超时?ScheduledExecutorService?
            // 超时
            if (System.currentTimeMillis() - start > timeout) {
                throw new RejectedExecutionException("Task " + taskId + " rejected,系统执行任务超时");
            }
            page = dataProvider.getPageData(currentPage++, 1000, context);
            if (!Objects.isNull(page) && Objects.nonNull(page.getRecords()) && !page.getRecords().isEmpty()) {
                dataProcessor.processData(page.getRecords());
            }
            // 更新导出进度
            updateTaskProgress(taskId, page);
        } while (page.hasNext());
        dataProcessor.end();
        log.info("executeExport cost:" + (System.currentTimeMillis() - start));
    }

    private void updateTaskFail(BigInteger taskId, String message) {
        Task entity = new Task();
        entity.setId(taskId);
        entity.setStatus("FAILED");
        entity.setErrMsg(message);
        taskService.updateExportTask(entity);
    }

    private void updateTaskStart(BigInteger taskId) {
        Task entity = new Task();
        entity.setId(taskId);
        entity.setStatus("导出中");
        entity.setStartTime(LocalDateTime.now());
        taskService.updateExportTask(entity);
    }

    private void updateTaskEnd(BigInteger taskId, String url) {
        Task entity = new Task();
        entity.setId(taskId);
        entity.setStatus("SUCCESS");
        entity.setErrMsg(null);
        entity.setEndTime(LocalDateTime.now());
        entity.setUrl(url);
        taskService.updateExportTask(entity);
    }

    // todo
    // 暂时将进度更新到数据库,可优化
    private void updateTaskProgress(BigInteger taskId, Page<?> page) {
        Task entity = new Task();
        entity.setId(taskId);
        if (page.getTotal() <= 0) {
            entity.setProgress(new BigDecimal(1));
        } else {
            entity.setProgress(new BigDecimal(page.getCurrent()).divide(new BigDecimal(page.getPages()), MATH_CONTEXT));
        }
        log.info("current:" + page.getCurrent() + " totalPage:" + page.getPages() + " progress " + entity.getProgress().toString());
        taskService.updateExportTask(entity);
    }

}

主要方法为:

Java 复制代码
public BigInteger asyncExport(ExportConfig config, DataProvider<?> dataProvider, DataProcessor dataProcessor, long timeout) {
}

DataProvider 数据提供接口

可根据分页分批提供数据

Java 复制代码
package com.async_export_demo.export;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

/**
 * 数据提供器接口(不同业务实现)
 */
public interface DataProvider<T> {
    Page<T> getPageData(long currentPage, long pageSize, AsyncExportService.ExportContext context);

    ExportEnum getType();
}

DataProvider 实现类

Java 复制代码
package com.async_export_demo.export;

import com.async_export_demo.mapper.OrderMapper;
import com.async_export_demo.model.Order;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderDataProvider implements DataProvider<Order> {

    @Autowired
    private OrderMapper orderMapper;

    @Override
    public Page<Order> getPageData(long currentPage, long pageSize, AsyncExportService.ExportContext context) {
        Page<Order> queryPage = new Page<>(currentPage, pageSize);
        if (currentPage > 1) {
            // 分页总数只需要查询一次
            queryPage.setSearchCount(false);
        }
        QueryWrapper<Order> queryWrapper = new QueryWrapper<>();
        // 只导出前100万
        queryWrapper.le("id", 4000000);
        Page<Order> result = orderMapper.selectPage(queryPage, queryWrapper);
        if (currentPage <= 1) {
            context.put("total", result.getTotal());
        } else {
            result.setTotal((Long) context.get("total"));
        }
        return result;
    }

    @Override
    public ExportEnum getType() {
        return ExportEnum.ORDER;
    }

}

DataProcessor 数据处理接口

Java 复制代码
package com.async_export_demo.export;

import java.util.List;

/**
 * 数据处理器接口(不同业务实现)
 */
public interface DataProcessor {
    void processData(List<?> data);
    void end();
    String getUrl();
}

DataProcessor数据处理实现类

Java 复制代码
package com.async_export_demo.export;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.async_export_demo.config.MyExportConfig;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedList;
import java.util.List;

public class DefaultDataProcessor implements DataProcessor {

    private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    /**
     * 一个sheet页最多允许的数据条数
     */
    private static final long SHEET_MAX_SIZE = 200000;

    private String absoluteFileName;

    private String fileName;

    private long processedDataCount = 0;

    private ExcelWriter excelWriter;

    private MyExportConfig exportConfig;

    private final LinkedList<WriteSheet> writeSheets = new LinkedList<>();

    public DefaultDataProcessor(MyExportConfig exportConfig, Class<?> head) {
        this.exportConfig = exportConfig;
        this.absoluteFileName = getFileName(exportConfig);
        this.excelWriter = EasyExcel.write(absoluteFileName, head).build();
    }

    @Override
    public void processData(List<?> data) {
        WriteSheet writeSheet = getWriteSheet(data.size());
        this.excelWriter.write(data, writeSheet);
        processedDataCount += data.size();
    }

    @Override
    public void end() {
        this.excelWriter.close();
    }

    @Override
    public String getUrl() {
        // todo
        // 实际可能需要将本地文件推送到文件服务器,最终从文件服务器下载,此处直接返回本地地址
        return exportConfig.getDomain() + "/" + fileName;
    }

    private String getFileName(MyExportConfig exportConfig) {
        String fileName = LocalDateTime.now().format(DTF) + ".xlsx";
        this.fileName = fileName;
        return exportConfig.getRootPath() + "\\" + fileName;
    }

    private WriteSheet getWriteSheet(long currentProcessCount) {
        if (processedDataCount == 0) {
            WriteSheet writeSheet = EasyExcel.writerSheet(0, "模板").build();
            writeSheets.add(writeSheet);
        } else {
            long count = processedDataCount + currentProcessCount - SHEET_MAX_SIZE * (writeSheets.size() - 1);
            if (count > SHEET_MAX_SIZE) {
                writeSheets.add(EasyExcel.writerSheet(writeSheets.size(), "模板" + writeSheets.size()).build());
            }
        }
        return writeSheets.getLast();
    }

}

Controller

Java 复制代码
package com.async_export_demo.controller;

import com.async_export_demo.export.AsyncExportService;
import com.async_export_demo.export.ExportEnum;
import com.async_export_demo.export.SyncExportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URLEncoder;

@RestController
@RequestMapping(value = "/export")
public class ExportController {

    @Autowired
    private AsyncExportService asyncExportService;

    @Autowired
    private SyncExportService syncExportService;

    @GetMapping(value = "/exportAsync")
    public String exportData(@RequestParam("type") ExportEnum exportEnum) {
        BigInteger taskId = asyncExportService.asyncExport(exportEnum);
        return taskId.toString();
    }

}

前端

html 复制代码
<!DOCTYPE html>
<!-- saved from url=(0018) -->
<html class="wide wow-animation desktop landscape rd-navbar-fullwidth-linked" lang="en">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title></title>
    <style>
        table, th, td {
            border: 1px solid black;
            border-collapse: collapse; /* 可选,用于合并相邻单元格的边框 */
        }

        th, td {
            padding: 10px; /* 可选,设置单元格内边距 */
        }
    </style>
</head>
<body>
<!-- Page-->
<div id="root">
    <button onclick="exportData()">导出</button>
    <table>
        <thead>
        <tr>
            <th style="width: 100px;text-align: left">任务id</th>
            <th style="width: 100px;text-align: left">类型</th>
            <th style="width: 100px;text-align: left">状态</th>
            <th style="width: 200px;text-align: left">创建时间</th>
            <th style="width: 200px;text-align: left">开始时间</th>
            <th style="width: 200px;text-align: left">结束时间</th>
            <th style="width: 200px;text-align: left">错误信息</th>
            <th style="width: 200px;text-align: left">下载地址</th>
            <th>进度</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="task ,taskIndex: ${tasks}">
            <td th:text="${task.id}">任务id</td>
            <td th:text="${task.type}">类型</td>
            <td th:text="${task.status}">状态</td>
            <td th:text="${task.createTime}">创建时间</td>
            <td th:text="${task.startTime}">开始时间</td>
            <td th:text="${task.endTime}">结束时间</td>
            <td th:text="${task.errMsg}">错误信息</td>
            <td>
                <a th:href="${task.url}" th:text="${task.url}">下载地址</a>
            </td>
            <td th:text="${task.progress}">进度</td>
        </tr>
        </tbody>
    </table>
</div>
<script>
    function exportData() {
        var xhr = new XMLHttpRequest();
        xhr.open('GET', '/export/exportAsync?type=ORDER', true);
        xhr.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                console.log(this.responseText); // 处理响应数据
            } else {
                console.error('Request failed:', this.statusText);
            }
        };
        xhr.onerror = function () {
            console.error('Request error');
        };
        xhr.send();
    }

    function refresh() {
        // 你的刷新逻辑
        console.log('刷新一次');
        location.reload();
        // 例如,你可以在这里进行页面局部刷新或者数据更新
        setTimeout(refresh, 4000); // 1000毫秒后再次调用refresh函数
    }

    // demo, 暴力刷新。。。。
    setTimeout(refresh, 4000); // 初始调用一次以开始循环
</script>

</body>
</html>

测试

代码地址:https://gitee.com/husong_zone/async_export_demo

相关推荐
羚羊角uou24 分钟前
【Linux】POSIX信号量、环形队列、基于环形队列实现生产者消费者模型
java·开发语言
代码萌新知7 小时前
设计模式学习(五)装饰者模式、桥接模式、外观模式
java·学习·设计模式·桥接模式·装饰器模式·外观模式
iナナ9 小时前
Spring Web MVC入门
java·前端·网络·后端·spring·mvc
未来之窗软件服务9 小时前
万象EXCEL开发(九)excel 高级混合查询 ——东方仙盟金丹期
大数据·excel·仙盟创梦ide·东方仙盟·万象excel
驱动探索者9 小时前
find 命令使用介绍
java·linux·运维·服务器·前端·学习·microsoft
卷Java9 小时前
违规通知功能修改说明
java·数据库·微信小程序·uni-app
CoderYanger9 小时前
优选算法-双指针:2.复写零
java·后端·算法·leetcode·职场和发展
小雨凉如水9 小时前
k8s学习-pod的生命周期
java·学习·kubernetes
李宥小哥10 小时前
C#基础10-结构体和枚举
java·开发语言·c#
领创工作室10 小时前
安卓设备分区作用详解-测试机红米K40
android·java·linux