适用于:基于 Spring Boot + MyBatis + Redis + RabbitMQ 等技术栈的企业级应用系统,提供统一的大数据量 Excel 报表导出能力,满足高性能、可复用、可观测等企业级要求。


0. 快速入门

本章节面向第一次接触该方案的开发人员,按照下面步骤即可从零搭建并跑通一个大数据量 Excel 导出功能。

  1. 引入依赖

    • pom.xml 中引入 EasyExcel 依赖(坐标以官网为准)。
    • 确保项目已经集成 Spring Boot Web、MyBatis、Redis、RabbitMQ 等基础组件。
  2. 创建导出任务表

    • 在业务库中执行文档第 4 章提供的 excel_export_task 建表 SQL。
    • 确认表所在数据源为读写库(后续导出任务状态需要更新)。
  3. 新增配置

    • application.yml 中增加 excel.export 配置段,并根据环境填写临时目录、访问域名等参数。
    • 在配置类中绑定 ExcelExportProperties,确保应用启动时能够正确加载这些配置。
  4. 实现业务导出 Handler

    • 针对某个导出场景(如“视频列表导出”或“订单列表导出”),实现一个 ExcelExportBizHandler
      • 定义导出行 DTO,并使用 EasyExcel 提供的注解(@ExcelProperty@DateTimeFormat@NumberFormat)标注列名、日期和数字格式。
      • buildPageQuery 中基于 MyBatis 实现分页查询逻辑。
  5. 接入异步链路

    • 在 RabbitMQ 中创建导出任务队列、交换机和路由键,并在应用中声明对应的 Bean。
    • 实现 ExcelExportConsumerExcelExportExecutor,消费导出任务消息并调用 EasyExcelWriter 执行导出。
  6. 暴露接口并联调

    • 在 Controller 中新增“提交导出任务”“查询任务进度”“下载文件”等接口。
    • 使用 Postman 或前端页面调用接口,完成从提交任务到下载 Excel 的端到端联调。

建议读者先按本节完成一次完整跑通,然后再结合后续章节的架构设计和性能、运维章节进行优化与扩展。

1. 设计目标与约束

1.1 功能目标
  • 通用性
    • 支持任意业务实体(用户、视频、评论、关注关系等)的 Excel 导出。
    • 通过通用导出组件 + 列映射机制,业务方仅需提供:查询条件 DTO、分页查询函数、列配置。
  • 异步化
    • 导出任务统一走异步链路,避免阻塞 HTTP 线程,前端通过任务 ID 轮询进度并获取下载链接。
  • 可观测性
    • 全链路进度监控:总行数、已导出行数、状态、耗时、错误信息等。
    • 支持日志记录与告警对接,便于排查问题。
1.2 性能与容量目标
  • 大数据量导出能力
    • 单任务支持 百万级行数 导出(例如 100 万行以上),不出现 OOM。
    • 单 Sheet 行数达到上限自动切换新 Sheet(例如 1,000,000 行 / Sheet)。
  • 性能目标
    • 在服务器资源合理配置下,100 万行导出耗时控制在数分钟内(视具体业务字段和网络 IO 而定)。
    • 支持多任务并行导出,通过线程池 + MQ 进行调度限流。
1.3 设计原则
  • 流式写入 + 分页查询:避免一次性将所有数据加载到内存。
  • 读写分离:导出链路尽量使用只读数据源,避免锁争用。
  • 解耦复用:导出核心组件与具体业务解耦,通过泛型 + 回调接口扩展。
  • 与现有架构保持一致
    • 控制器统一使用 Result<T> 作为响应结构。
    • 使用已有的 RedisTemplateRabbitTemplate、日志体系、配置方式。

2. 技术选型与依赖

2.1 EasyExcel 选型说明
  • EasyExcel 优势(参考官方文档 https://easyexcel.opensource.alibaba.com/docs/current/api/):
    • 高性能、低内存:基于 SAX 解析与流式读写,相比传统 POI,在大数据量场景下显著降低内存占用。
    • 注解驱动:通过 @ExcelProperty 等注解即可完成 Excel 头与实体字段的映射,简化模型定义。
    • API 简单:典型写入代码为 EasyExcel.write(...).sheet(...).doWrite(...),非常适合封装为通用导出组件并集成到现有 Spring Boot 项目中。
    • 大规模数据友好:支持多 Sheet 写入、分批写入等模式,适合与分页查询、异步任务、分布式任务调度等模式结合,实现稳定的企业级报表导出能力。
2.2 Maven 依赖示例
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>最新稳定版本</version>
</dependency>

依赖坐标和版本号请以 EasyExcel 官网与 Maven Central 为准,建议在 pom.xml 中统一集中管理。


3. 总体架构设计

3.1 组件分层
  • 接口层(Controller)

    • 暴露导出任务提交接口、任务进度查询接口、文件下载接口。
    • 使用 Result<T> 封装统一响应。
  • 应用服务层(ExcelExportAppService)

    • 处理导出提交、参数校验、任务创建、发送 MQ 消息等应用逻辑。
    • 聚合调用通用导出核心服务、任务管理服务。
  • 导出核心层(ExcelExportCore / EasyExcelWriter)

    • 基于 EasyExcel 封装分页写入、Sheet 管理、列映射与格式化。
    • 对外提供通用导出入口:export(ExcelExportContext<T> context)
  • 任务管理层(ExportTaskManager)

    • 负责导出任务的生命周期管理:创建、更新、完成、失败。
    • 持久化到数据库(如 excel_export_task 表),同时将进度信息写入 Redis 以支持高并发查询。
  • 异步执行层(MQ 消费者)

    • 基于 RabbitMQ 的导出任务队列:
      • 提交任务时发送消息到导出队列。
      • 消费者从队列中取出任务,调用导出核心执行,更新进度与最终结果。
  • 文件存储层

    • 导出文件写入本地磁盘 / NFS / 对象存储(如 OSS、S3),生成可访问 URL。
    • 通过配置项统一控制存储路径与访问前缀。
3.2 典型流程
  1. 提交导出请求
    • 前端携带查询条件(如时间范围、用户 ID 等)调用 POST /api/export/videos
  2. 创建导出任务
    • Controller 调用 ExcelExportAppService.submitExportTask()
    • 生成 taskId,落库记录初始状态(PENDING),写入部分参数(导出类型、查询条件快照、创建人等)。
    • 将简要任务信息缓存到 Redis。
    • 发送导出任务消息到 RabbitMQ。
  3. 异步执行导出
    • MQ 消费者收到任务后,更新任务状态为 RUNNING
    • 按配置好的分页大小,通过 MyBatis 分页查询数据。
    • 将每一页数据交给 EasyExcelWriter 流式写入 Excel 文件。
    • 每处理一页或一定行数,实时更新 Redis 中的进度信息(已导出行数、百分比等),并周期性更新数据库。
  4. 完成导出
    • 所有数据写完后,关闭流,生成最终文件。
    • 更新任务状态为 SUCCESS,记录文件路径 / 下载 URL。
  5. 前端轮询进度 & 下载
    • 前端通过 GET /api/export/tasks/{taskId} 查询进度。
    • 若状态为 SUCCESS,调用 GET /api/export/download/{taskId} 获取文件流或重定向下载 URL。

4. 核心领域模型与通用组件设计

4.1 导出任务实体 ExcelExportTask
  • 表结构建议(示例)
CREATE TABLE excel_export_task (
    id              BIGINT      PRIMARY KEY,
    task_code       VARCHAR(64) NOT NULL COMMENT '任务唯一标识',
    biz_type        VARCHAR(64) NOT NULL COMMENT '业务类型,如 VIDEO_LIST、USER_LIST',
    file_name       VARCHAR(255) NOT NULL COMMENT '导出文件名',
    file_path       VARCHAR(512) DEFAULT NULL COMMENT '文件存储路径',
    status          VARCHAR(32)  NOT NULL COMMENT '任务状态:PENDING/RUNNING/SUCCESS/FAILED',
    total_count     BIGINT       DEFAULT 0 COMMENT '总记录数(可选,预估值或首轮统计值)',
    success_count   BIGINT       DEFAULT 0 COMMENT '已导出记录数',
    fail_reason     VARCHAR(1024) DEFAULT NULL COMMENT '失败原因',
    query_params    TEXT         DEFAULT NULL COMMENT '查询条件快照(JSON)',
    created_by      BIGINT       DEFAULT NULL COMMENT '提交人用户ID',
    create_time     DATETIME     NOT NULL,
    update_time     DATETIME     NOT NULL
) COMMENT='Excel 导出任务表';
  • 对应实体类(简化示例)
@Data
public class ExcelExportTask {
    private Long id;
    private String taskCode;
    private String bizType;
    private String fileName;
    private String filePath;
    private String status;      // PENDING/RUNNING/SUCCESS/FAILED
    private Long totalCount;
    private Long successCount;
    private String failReason;
    private String queryParams; // JSON
    private Long createdBy;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
  • 使用 MyBatis 在 mapper 包下定义 ExcelExportTaskMapper 进行 CRUD 操作。
4.2 通用分页查询接口
  • 分页参数模型
@Data
public class PageParam {
    private long pageNo;
    private long pageSize;
    private Long lastId; // 可选,用于游标分页
}
  • 分页查询函数接口
@FunctionalInterface
public interface ExcelPageQuery<T> {
    List<T> query(PageParam pageParam);
}

业务侧只需要实现 ExcelPageQuery<T>,内部使用 MyBatis 分页查询即可。

4.3 列映射与格式化
  • 注解驱动的列配置(推荐)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelColumn {
    String title();            // 列标题
    int order();               // 列顺序
    int width() default 20;    // 列宽(字符数)
    String format() default ""; // 日期/数字格式
}
  • 示例 DTO
@Data
public class VideoExportRowDTO {

    @ExcelColumn(title = "视频ID", order = 0, width = 20, format = "")
    @ExcelProperty(value = "视频ID", index = 0)
    @NumberFormat("#")
    private Long id;

    @ExcelColumn(title = "视频标题", order = 1, width = 40, format = "")
    @ExcelProperty(value = "视频标题", index = 1)
    private String title;

    @ExcelColumn(title = "作者ID", order = 2, width = 20, format = "")
    @ExcelProperty(value = "作者ID", index = 2)
    @NumberFormat("#")
    private Long userId;

    @ExcelColumn(title = "发布时间", order = 3, width = 25, format = "yyyy-MM-dd HH:mm:ss")
    @ExcelProperty(value = "发布时间", index = 3)
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    private LocalDateTime publishTime;
}
  • 通用列解析工具类
public class ExcelColumnHelper {

    public static List<ExcelColumnMeta> resolveColumns(Class<?> clazz) {
        List<ExcelColumnMeta> result = new ArrayList<>();
        for (Field field : clazz.getDeclaredFields()) {
            ExcelColumn column = field.getAnnotation(ExcelColumn.class);
            if (column == null) {
                continue;
            }
            ExcelColumnMeta meta = new ExcelColumnMeta();
            meta.setFieldName(field.getName());
            meta.setTitle(column.title());
            meta.setOrder(column.order());
            meta.setWidth(column.width());
            meta.setFormat(column.format());
            result.add(meta);
        }
        // 按列顺序排序,保证生成的列顺序稳定
        result.sort(Comparator.comparingInt(ExcelColumnMeta::getOrder));
        return result;
    }

    private ExcelColumnHelper() {
    }
}

@Data
public class ExcelColumnMeta {
    private String fieldName;
    private String title;
    private int order;
    private int width;
    private String format;
}

如需更灵活的列配置(动态选择列),可在任务提交时由前端传入列配置 JSON,服务端合并注解默认值与动态配置。

4.4 导出上下文 ExcelExportContext<T>
@Data
public class ExcelExportContext<T> {
    private String taskCode;
    private String bizType;
    private String fileName;
    private String sheetName;
    private Class<T> rowClass;
    private ExcelPageQuery<T> pageQuery;
    private long pageSize;
    private long maxRowsPerSheet;
    private OutputStream outputStream;
}

5. EasyExcel 导出执行器设计

5.1 核心类 EasyExcelWriter<T>
  • 职责

    • 基于 ExcelExportContext<T>,使用 EasyExcel 流式写出 Excel 文件。
    • 控制分页写入、Sheet 管理以及进度回调。
  • 伪代码示例

public class EasyExcelWriter<T> {

    public void write(ExcelExportContext<T> context, Consumer<Long> progressCallback) {
        ExcelWriter excelWriter = null;
        try {
            excelWriter = EasyExcel
                    .write(context.getOutputStream(), context.getRowClass())
                    .autoCloseStream(false)
                    .build();

            WriteSheet writeSheet = EasyExcel
                    .writerSheet(0, context.getSheetName())
                    .build();

            long totalWritten = 0L;
            long pageNo = 1;

            while (true) {
                PageParam pageParam = new PageParam();
                pageParam.setPageNo(pageNo);
                pageParam.setPageSize(context.getPageSize());

                List<T> dataList = context.getPageQuery().query(pageParam);
                if (dataList == null || dataList.isEmpty()) {
                    break;
                }

                // 如需控制单个 Sheet 最大行数(context.getMaxRowsPerSheet),可在此处根据行数拆分到多个 Sheet
                excelWriter.write(dataList, writeSheet);

                totalWritten += dataList.size();
                if (progressCallback != null) {
                    progressCallback.accept(totalWritten);
                }

                pageNo++;
            }
        } catch (Exception e) {
            throw new ExcelExportException("EasyExcel 导出失败", e);
        } finally {
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    }
}

ReflectUtilsformatValue 等工具方法可根据项目需要自行封装;以上代码为伪代码示例,具体 API 使用方式请以 EasyExcel 官方文档为准(参考 https://easyexcel.opensource.alibaba.com/docs/current/api/)。

5.2 内存与性能优化
  • 分页大小控制
    • 推荐 1000 ~ 5000 行 / 页,可通过配置项调整。
  • Sheet 分割
    • maxRowsPerSheet 可配置如 1_000_000,超过后创建新 Sheet,防止单 Sheet 过大。
  • 避免中间集合复制
    • 直接遍历 MyBatis 查询返回的 List<T> 写入 Excel,不再二次复制。
  • 日志与监控
    • 每处理 N 页打印一次 INFO 日志(包含 taskCode、已导出行数、耗时等)。

6. 任务管理、进度监控与状态反馈

6.1 Redis 进度缓存模型
  • Key 设计

    • excel:task:{taskCode}
  • Value 示例(JSON)

{
  "taskCode": "EXPORT_VIDEO_20250101120000",
  "status": "RUNNING",
  "totalCount": 1000000,
  "successCount": 520000,
  "percent": 52.0,
  "fileUrl": null,
  "errorMsg": null,
  "startTime": "2025-01-01T12:00:00",
  "updateTime": "2025-01-01T12:10:30"
}
  • 使用已有 RedisTemplate<String, Object> 进行读写,设置 TTL(如 24 小时)。
6.2 进度更新策略
  • 导出执行过程中:
    • 每处理一页数据时,根据 successCount / totalCount 更新 Redis 中的进度百分比。
    • 每隔一段时间(如 30 秒)或处理一定页数时更新数据库中的进度字段,保证持久化。
  • 任务完成:
    • Redis 中写入 status = SUCCESS,设置 fileUrl
    • DB 中更新 status = SUCCESSfilePath 字段。
  • 任务失败:
    • 记录失败原因 failReasonstatus = FAILED,并写入 Redis 方便前端展示。
6.3 进度查询接口
  • 接口示例
@RestController
@RequestMapping("/api/export")
public class ExcelExportController {

    @GetMapping("/tasks/{taskCode}")
    public Result<ExportTaskProgressVO> getTaskProgress(@PathVariable String taskCode) {
        ExportTaskProgressVO vo = exportTaskQueryService.queryProgress(taskCode);
        return Result.success(vo);
    }
}
  • 返回 VO 示例
@Data
public class ExportTaskProgressVO {
    private String taskCode;
    private String status;       // PENDING/RUNNING/SUCCESS/FAILED
    private Long totalCount;
    private Long successCount;
    private BigDecimal percent;
    private String fileUrl;
    private String errorMsg;
}

7. 异步执行、RabbitMQ 集成与重试机制

7.1 MQ 资源规划
  • 新增导出相关的交换机、队列、路由键(可在现有 RabbitMQConfig 基础上扩展):
public static final String EXPORT_QUEUE = "excel.export.queue";
public static final String EXPORT_EXCHANGE = "excel.export.exchange";
public static final String EXPORT_ROUTING_KEY = "excel.export.routing";
  • 配置持久化队列 + 死信队列,避免消息丢失。
7.2 任务提交与消息发送
@Service
public class ExcelExportAppService {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private ExcelExportTaskManager taskManager;

    public String submitExportTask(ExcelExportSubmitCommand command) {
        // 1. 创建任务记录
        ExcelExportTask task = taskManager.createTask(command);

        // 2. 发送 MQ 消息
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.EXPORT_EXCHANGE,
                RabbitMQConfig.EXPORT_ROUTING_KEY,
                new ExcelExportMessage(task.getTaskCode(), task.getBizType()));

        return task.getTaskCode();
    }
}
7.3 MQ 消费者与执行器
@Service
public class ExcelExportConsumer {

    @Resource
    private ExcelExportExecutor excelExportExecutor;

    @RabbitListener(queues = RabbitMQConfig.EXPORT_QUEUE)
    public void onMessage(ExcelExportMessage message) {
        excelExportExecutor.execute(message.getTaskCode());
    }
}
  • ExcelExportExecutor 内部逻辑:
    • 根据 taskCode 查询任务、反序列化查询条件。
    • 通过业务注册中心找到对应的 ExcelPageQuery<T>rowClass
    • 构建 ExcelExportContext<T>,打开输出流(文件)。
    • 调用 EasyExcelWriter.write() 实际导出,同时通过回调更新进度。
7.4 异常处理与重试
  • 执行异常
    • 捕获所有异常,更新任务状态为 FAILED,记录 failReason
    • 将错误栈记录到日志中,方便排查。
  • 重试机制
    • 通过 MQ 死信队列 + 人工重放;
    • 或在 ExcelExportExecutor.execute 上使用 @Retryable(适度控制重试次数与间隔)。
  • 幂等性
    • 执行导出前检查任务状态:
      • 若已为 SUCCESS,直接返回;
      • 若为 RUNNING,拒绝重复执行;
      • 若为 FAILED,按业务规则决定是否允许重新提交。

8. 控制器接口设计

8.1 提交导出任务接口
  • 接口定义
@RestController
@RequestMapping("/api/export")
public class ExcelExportController {

    @PostMapping("/videos")
    public Result<String> exportVideos(@RequestBody VideoExportQueryDTO queryDTO) {
        String taskCode = excelExportAppService.submitVideoExportTask(queryDTO);
        return Result.success(taskCode);
    }
}
  • 请求 DTO 示例
@Data
public class VideoExportQueryDTO {
    private Long userId;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Integer minLikes;
}
8.2 进度查询接口
  • 参考第 6 节 getTaskProgress 示例。
8.3 文件下载接口
  • 接口定义
@GetMapping("/download/{taskCode}")
public void download(@PathVariable String taskCode, HttpServletResponse response) throws IOException {
    ExcelExportTask task = taskManager.findByTaskCode(taskCode);
    if (task == null || !"SUCCESS".equals(task.getStatus())) {
        // 可以抛业务异常或返回友好错误
        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "任务未完成或不存在");
        return;
    }

    File file = new File(task.getFilePath());
    if (!file.exists()) {
        response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
        return;
    }

    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(task.getFileName(), "UTF-8"));

    try (InputStream in = new FileInputStream(file);
         OutputStream out = response.getOutputStream()) {
        in.transferTo(out);
        out.flush();
    }
}

若使用对象存储,可直接返回外链 URL,由前端自行下载。


9. 配置项设计

9.1 application.yml 示例
excel:
  export:
    tmp-dir: /data/export/tmp           # 导出文件临时目录
    base-url: https://static.xxx.com/export/  # 文件访问前缀(对象存储/静态资源域名)
    page-size: 2000                     # 分页大小
    max-rows-per-sheet: 1000000         # 单 Sheet 最大行数
    task-expire-hours: 24               # 任务过期时间(用于清理)
    max-concurrent-tasks: 5             # 最大并行导出任务数
    enable-async: true                  # 是否异步导出
    mq:
      exchange: excel.export.exchange
      queue: excel.export.queue
      routing-key: excel.export.routing
  • 参数说明建议
    • tmp-dir:导出文件临时存放目录,应指向磁盘空间相对充足且 I/O 性能较好的路径。
    • base-url:暴露给前端的文件访问前缀,本地环境可为 http://localhost:8080/export/,生产环境通常为 CDN 或对象存储域名。
    • page-size:单次查询的行数,一般推荐 1000–5000,根据数据库和网络性能适当调整。
    • max-rows-per-sheet:单个 Sheet 最大行数,超过后自动换 Sheet,避免单 Sheet 过大导致 Excel 打开缓慢。
    • task-expire-hours:导出任务与文件的过期时间,将由定时任务或对象存储生命周期策略清理。
    • max-concurrent-tasks:同一实例允许同时执行的导出任务数量,用于防止导出任务过多导致资源被抢占。
    • enable-async:是否启用异步导出;关闭时可在开发环境使用同步导出便于调试。
    • mq.exchange / mq.queue / mq.routing-key:导出任务使用的 RabbitMQ 交换机、队列和路由键名称,应与 MQ 配置保持一致。
9.2 与项目配置风格对齐
  • 在配置模块(例如 com.example.config 包)下新增 ExcelExportProperties
@Data
@ConfigurationProperties(prefix = "excel.export")
public class ExcelExportProperties {
    private String tmpDir;
    private String baseUrl;
    private int pageSize = 2000;
    private long maxRowsPerSheet = 1000000L;
    private int taskExpireHours = 24;
    private int maxConcurrentTasks = 5;
    private boolean enableAsync = true;
    private MqProperties mq;

    @Data
    public static class MqProperties {
        private String exchange;
        private String queue;
        private String routingKey;
    }
}
  • 在启动类或配置类上添加 @EnableConfigurationProperties(ExcelExportProperties.class),保持与现有项目配置方式一致。

10. 使用示例:视频列表导出

10.1 注册业务导出处理器
  • 定义注册中心,维护 bizType -> 处理器 的映射:
public interface ExcelExportBizHandler<T> {
    String getBizType();
    Class<T> getRowClass();
    ExcelPageQuery<T> buildPageQuery(String queryParamsJson);
}
  • 视频导出实现:
@Component
public class VideoExportBizHandler implements ExcelExportBizHandler<VideoExportRowDTO> {

    @Resource
    private VideoMapper videoMapper;

    @Override
    public String getBizType() {
        return "VIDEO_LIST";
    }

    @Override
    public Class<VideoExportRowDTO> getRowClass() {
        return VideoExportRowDTO.class;
    }

    @Override
    public ExcelPageQuery<VideoExportRowDTO> buildPageQuery(String queryParamsJson) {
        VideoExportQueryDTO queryDTO = JsonUtils.fromJson(queryParamsJson, VideoExportQueryDTO.class);
        return pageParam -> {
            // 使用 MyBatis 分页查询
            int offset = (int) ((pageParam.getPageNo() - 1) * pageParam.getPageSize());
            List<Video> videos = videoMapper.selectByCondition(queryDTO, offset, (int) pageParam.getPageSize());
            return videos.stream().map(this::toRowDTO).collect(Collectors.toList());
        };
    }

    private VideoExportRowDTO toRowDTO(Video video) {
        VideoExportRowDTO dto = new VideoExportRowDTO();
        dto.setId(video.getId());
        dto.setTitle(video.getTitle());
        dto.setUserId(video.getUserId());
        dto.setPublishTime(video.getCreateTime());
        return dto;
    }
}
10.2 提交任务与执行
  • Controller 调用 excelExportAppService.submitVideoExportTask(queryDTO)
    • 序列化 queryDTO 为 JSON 存入 ExcelExportTask.queryParams
    • bizType 设置为 VIDEO_LIST
  • MQ 消费者侧:
    • 根据 bizType 从注册中心找到 VideoExportBizHandler
    • 使用 buildPageQuery 构建分页查询函数。
    • 组合 ExcelExportContext 并调用 EasyExcelWriter 实际导出。
10.3 常见使用场景操作步骤

以“导出视频列表报表”为例,一个典型操作流程如下:

  1. 业务方在页面选择筛选条件(时间范围、作者、点赞数等),点击“导出 Excel”。
  2. 前端调用 POST /api/export/videos 提交导出任务,并展示返回的 taskCode
  3. 前端每隔若干秒调用 GET /api/export/tasks/{taskCode} 轮询进度条(根据 percent 字段展示进度)。
  4. 当状态为 SUCCESS 时,前端展示“下载”按钮,调用 GET /api/export/download/{taskCode} 拉起浏览器下载。
  5. 用户打开 Excel 文件进行查看或进一步分析。

其他业务场景(如订单导出、用户导出)仅需:

  • 定义对应的导出行 DTO 和 @ExcelColumn 注解;
  • ExcelExportBizHandler 中实现分页查询逻辑;
  • 在 Controller 中增加对应的导出接口。

11. 测试与验证方案

11.1 单元测试
  • 列解析工具类 进行测试:
    • 校验 @ExcelColumn 注解解析是否正确(标题、顺序、格式等)。
  • EasyExcelWriter 进行小数据量单测:
    • 构造内存输出流 ByteArrayOutputStream,写入少量数据,校验生成的文件结构(如行数、表头内容)。
11.2 集成测试
  • 构建专用测试用例:
    • 使用测试库(如 H2 或测试 MySQL)插入 1 万条以上视频数据。
    • 调用导出执行器,验证导出成功、进度更新正确、文件可正常打开。
  • 对 MQ 链路:
    • 使用 Spring AMQP 提供的测试工具或使用内存队列模拟,验证消息收发与任务执行流程。
11.3 性能与压力测试
  • 在接近生产环境的机器上:
    • 准备 100 万条以上数据,执行导出任务。
    • 记录总耗时、CPU/内存占用、磁盘 IO、网络带宽等指标。
    • 调整 page-sizemax-concurrent-tasks 等配置,找到平衡点。
11.4 测试示例代码

下面给出一个基于 JUnit 的简化单元测试示例,用于验证 EasyExcelWriter 能够正确将少量数据写入到内存流中:

@ExtendWith(SpringExtension.class)
public class EasyExcelWriterTest {

    @Test
    void testWriteSmallDataSet() throws Exception {
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        ExcelExportContext<DemoRowDTO> context = new ExcelExportContext<>();
        context.setTaskCode("TEST_TASK");
        context.setBizType("DEMO");
        context.setFileName("demo.xlsx");
        context.setSheetName("测试");
        context.setRowClass(DemoRowDTO.class);
        context.setPageSize(100);
        context.setMaxRowsPerSheet(1000);
        context.setOutputStream(out);
        context.setPageQuery(pageParam -> {
            if (pageParam.getPageNo() > 1) {
                return Collections.emptyList();
            }
            List<DemoRowDTO> list = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                DemoRowDTO row = new DemoRowDTO();
                row.setId((long) i + 1);
                row.setName("demo-" + i);
                list.add(row);
            }
            return list;
        });

        EasyExcelWriter<DemoRowDTO> writer = new EasyExcelWriter<>();
        writer.write(context, null);

        // 仅做简单断言:输出流不为空
        Assertions.assertTrue(out.size() > 0);
    }

    @Data
    public static class DemoRowDTO {

        @ExcelProperty(value = "ID", index = 0)
        @NumberFormat("#")
        private Long id;

        @ExcelProperty(value = "名称", index = 1)
        private String name;
    }
}

集成测试可在 SpringBootTest 环境下,通过真实的数据库、Redis、RabbitMQ 实例来验证完整链路(提交任务 → 消费消息 → 生成文件 → 下载文件)。

11.5 性能测试方法与预期指标
  • 测试步骤建议

    • 准备不同规模的数据集(如 10 万、50 万、100 万行),分别执行导出任务。
    • 每次测试记录:
      • 导出总耗时;
      • 应用实例的 CPU / 内存峰值;
      • 数据库 QPS 与慢查询情况;
      • 磁盘写入速率与网络带宽利用率(如文件上传到对象存储)。
    • 在不同配置组合下对比效果,例如:
      • page-size 分别为 1000 / 2000 / 5000;
      • max-concurrent-tasks 分别为 1 / 3 / 5;
      • 不同磁盘路径(本地 SSD、网络盘)。
  • 参考指标(需根据实际环境调整)

    • 100 万行导出耗时控制在数分钟级别(如 3–10 分钟);
    • 导出过程中应用实例内存占用保持在可接受范围内,不出现长时间 Full GC 或 OOM;
    • 数据库与 Redis 等核心依赖的负载在运维可控范围内,不影响主业务请求。

12. 运维与安全考虑

12.1 文件生命周期管理
  • 定期清理过期导出文件:
    • 根据 task-expire-hours 计算过期时间,定时任务删除本地文件并标记任务状态(如 EXPIRED)。
  • 对象存储场景下,可配置生命周期策略自动删除。
12.2 权限与安全
  • 导出接口需校验用户身份与权限:
    • 仅允许登录用户访问,与现有 JWT / 用户上下文体系对齐。
    • 对部分敏感报表(包含隐私数据)增加角色/权限校验。
  • 导出数据需进行敏感信息脱敏:
    • 利用项目现有的脱敏组件(mask),在生成 RowDTO 时进行处理。
12.3 监控与告警
  • 日志:
    • 为每个导出任务记录关键日志:提交、开始执行、进度更新、完成/失败。
  • 指标:
    • 暴露导出任务数量、成功率、平均耗时等指标,接入监控系统。
  • 告警:
    • 当导出失败率过高或任务执行时间异常时,触发告警。

13. 与现有模块的协调性

  • MyBatis
    • 分页查询完全基于现有 Mapper 扩展,无需更改现有业务逻辑。
  • Redis
    • 使用已有的 RedisTemplate 进行任务进度缓存,保持序列化方式一致。
  • RabbitMQ
    • 复用现有 RabbitTemplate 与监听容器工厂,只需新增交换机和队列配置。
  • 统一响应 Result
    • 所有导出相关接口(提交、进度查询、下载链接获取)全部使用 Result<T> 包装返回。

通过以上设计,可以在不破坏现有系统整体架构的前提下,快速落地一个 可复用、可扩展、可观测 的大数据量 Excel 导出体系,满足多业务线报表导出需求。


14. 常见问题 FAQ 与故障排查

  • Q1:导出任务执行时出现内存溢出(OOM)怎么办?

    • 检查点
      • page-size 是否过大,可尝试从 5000 调整为 1000–2000;
      • 是否在分页查询后进行了不必要的集合复制或缓存(应避免在内存中保留全部数据)。
    • 建议
      • 优先采用流式写入模式,不在内存中构建完整工作簿或全部数据列表;
      • 对导出内容进行精简,避免一次性导出过多无关字段。
  • Q2:导出的 Excel 文件无法打开或提示损坏?

    • 检查点
      • 是否正确关闭了工作簿和输出流;
      • 导出过程中是否发生异常导致文件写入中断,但任务状态未正确标记为失败。
    • 建议
      • 在导出完成后确保调用 excelWriter.finish() 或使用 try-with-resources 正确关闭相关资源;
      • 捕获异常时将任务标记为 FAILED,并记录详细错误日志。
  • Q3:前端进度条长时间停留在某个百分比不动?

    • 检查点
      • 导出执行器中是否按页数定期更新 Redis 进度;
      • 是否存在长时间单页查询(SQL 过慢)或磁盘 I/O 瓶颈。
    • 建议
      • 将进度更新逻辑抽取为统一工具方法,在每页处理完成后调用;
      • 配合慢查询日志与磁盘监控定位瓶颈。
  • Q4:同一个导出任务被重复执行或重复下载?

    • 检查点
      • 是否在执行前检查任务状态,避免对 SUCCESS 状态的任务重复执行;
      • 前端是否对“导出”按钮做了防重提交处理。
    • 建议
      • ExcelExportExecutor 中对任务状态进行严格校验;
      • 在提交任务接口中引入幂等控制(如基于业务唯一键或请求幂等号)。

15. 性能调优与最佳实践

  • 数据库与分页

    • 为导出查询涉及的过滤字段建立合适的索引,减少全表扫描;
    • 在数据量极大的场景下,可优先考虑基于主键或时间字段的“游标分页”而非传统 OFFSET 分页。
  • 线程池与并发控制

    • 为导出任务使用独立的线程池,避免与核心业务线程池混用;
    • 通过配置 max-concurrent-tasks 控制并发导出数量,防止压垮数据库和磁盘。
  • 文件与存储

    • 将导出临时目录放在 I/O 性能较好的磁盘(优先选择本地 SSD);
    • 大文件可优先上传至对象存储,前端通过外链下载,减少应用实例带宽压力。
  • 监控与可观测性

    • 为导出任务埋点记录关键指标,如开始时间、结束时间、总行数、总耗时等;
    • 为导出失败、执行时间异常等情况设置告警阈值。

16. 部署与验证检查清单

  • 基础环境

    • 应用已正确引入 EasyExcel 及相关依赖;
    • 数据库中已创建 excel_export_task 表并通过权限校验;
    • Redis、RabbitMQ 等基础组件已可用,并与应用配置一致。
  • 配置与存储

    • application.yml 中的 excel.export 配置已根据环境填写并通过启动校验;
    • 导出临时目录存在且具备读写权限;
    • 对象存储或静态资源服务已配置好访问域名(如使用 base-url)。
  • 功能验证

    • 提交导出任务接口可以正常返回任务编号;
    • 进度查询接口能够随导出进展更新百分比;
    • 导出完成后可正常下载并打开 Excel 文件,数据条数与预期一致。
  • 稳定性与回滚

    • 在测试环境完成至少一次 10 万行以上的导出压测,确认系统稳定;
    • 若上线后发现问题,有明确的配置开关或回滚方案(如关闭异步导出、下线导出入口等)。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐