大数据量 Excel 导出企业级最佳实践方案(基于 EasyExcel)
文章目录
适用于:基于 Spring Boot + MyBatis + Redis + RabbitMQ 等技术栈的企业级应用系统,提供统一的大数据量 Excel 报表导出能力,满足高性能、可复用、可观测等企业级要求。
0. 快速入门
本章节面向第一次接触该方案的开发人员,按照下面步骤即可从零搭建并跑通一个大数据量 Excel 导出功能。
-
引入依赖
- 在
pom.xml中引入 EasyExcel 依赖(坐标以官网为准)。 - 确保项目已经集成 Spring Boot Web、MyBatis、Redis、RabbitMQ 等基础组件。
- 在
-
创建导出任务表
- 在业务库中执行文档第 4 章提供的
excel_export_task建表 SQL。 - 确认表所在数据源为读写库(后续导出任务状态需要更新)。
- 在业务库中执行文档第 4 章提供的
-
新增配置
- 在
application.yml中增加excel.export配置段,并根据环境填写临时目录、访问域名等参数。 - 在配置类中绑定
ExcelExportProperties,确保应用启动时能够正确加载这些配置。
- 在
-
实现业务导出 Handler
- 针对某个导出场景(如“视频列表导出”或“订单列表导出”),实现一个
ExcelExportBizHandler:- 定义导出行 DTO,并使用 EasyExcel 提供的注解(
@ExcelProperty、@DateTimeFormat、@NumberFormat)标注列名、日期和数字格式。 - 在
buildPageQuery中基于 MyBatis 实现分页查询逻辑。
- 定义导出行 DTO,并使用 EasyExcel 提供的注解(
- 针对某个导出场景(如“视频列表导出”或“订单列表导出”),实现一个
-
接入异步链路
- 在 RabbitMQ 中创建导出任务队列、交换机和路由键,并在应用中声明对应的 Bean。
- 实现
ExcelExportConsumer和ExcelExportExecutor,消费导出任务消息并调用EasyExcelWriter执行导出。
-
暴露接口并联调
- 在 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>作为响应结构。 - 使用已有的
RedisTemplate、RabbitTemplate、日志体系、配置方式。
- 控制器统一使用
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 的导出任务队列:
- 提交任务时发送消息到导出队列。
- 消费者从队列中取出任务,调用导出核心执行,更新进度与最终结果。
- 基于 RabbitMQ 的导出任务队列:
-
文件存储层
- 导出文件写入本地磁盘 / NFS / 对象存储(如 OSS、S3),生成可访问 URL。
- 通过配置项统一控制存储路径与访问前缀。
3.2 典型流程
- 提交导出请求:
- 前端携带查询条件(如时间范围、用户 ID 等)调用
POST /api/export/videos。
- 前端携带查询条件(如时间范围、用户 ID 等)调用
- 创建导出任务:
- Controller 调用
ExcelExportAppService.submitExportTask()。 - 生成
taskId,落库记录初始状态(PENDING),写入部分参数(导出类型、查询条件快照、创建人等)。 - 将简要任务信息缓存到 Redis。
- 发送导出任务消息到 RabbitMQ。
- Controller 调用
- 异步执行导出:
- MQ 消费者收到任务后,更新任务状态为
RUNNING。 - 按配置好的分页大小,通过 MyBatis 分页查询数据。
- 将每一页数据交给
EasyExcelWriter流式写入 Excel 文件。 - 每处理一页或一定行数,实时更新 Redis 中的进度信息(已导出行数、百分比等),并周期性更新数据库。
- MQ 消费者收到任务后,更新任务状态为
- 完成导出:
- 所有数据写完后,关闭流,生成最终文件。
- 更新任务状态为
SUCCESS,记录文件路径 / 下载 URL。
- 前端轮询进度 & 下载:
- 前端通过
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();
}
}
}
}
ReflectUtils和formatValue等工具方法可根据项目需要自行封装;以上代码为伪代码示例,具体 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,不再二次复制。
- 直接遍历 MyBatis 查询返回的
- 日志与监控:
- 每处理 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 = SUCCESS,filePath字段。
- Redis 中写入
- 任务失败:
- 记录失败原因
failReason,status = 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 常见使用场景操作步骤
以“导出视频列表报表”为例,一个典型操作流程如下:
- 业务方在页面选择筛选条件(时间范围、作者、点赞数等),点击“导出 Excel”。
- 前端调用
POST /api/export/videos提交导出任务,并展示返回的taskCode。 - 前端每隔若干秒调用
GET /api/export/tasks/{taskCode}轮询进度条(根据percent字段展示进度)。 - 当状态为
SUCCESS时,前端展示“下载”按钮,调用GET /api/export/download/{taskCode}拉起浏览器下载。 - 用户打开 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-size、max-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时进行处理。
- 利用项目现有的脱敏组件(mask),在生成
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 万行以上的导出压测,确认系统稳定;
- 若上线后发现问题,有明确的配置开关或回滚方案(如关闭异步导出、下线导出入口等)。
更多推荐


所有评论(0)