在企业级开发中,Excel 导出是高频需求,而当数据量达到10万级甚至百万级时,传统的一次性导出方案会面临内存溢出(OOM)、请求超时、下载缓慢等问题。本文将详细介绍一套基于 EasyExcel + MinIO + ZIP 的大数据量 Excel 导出解决方案,支持分批导出、异步处理、压缩打包、安全存储与下载,完美解决大数据量导出的痛点。
一、方案整体设计
1. 方案核心目标
- 支持百万级数据导出,避免 OOM 问题;
- 异步处理导出任务,避免前端请求超时;
- 生成的文件安全存储,支持带权限的过期下载;
- 多文件分批生成后压缩打包,提升用户下载体验;
- 代码规范、可复用,适配微服务架构。
2. 技术栈详细说明
| 组件 | 版本 | 核心作用 | 选择理由 | | --- | --- | --- | --- | | EasyExcel | 3.3.3 | 高性能 Excel 读写 | 阿里巴巴开源,针对 POI 的性能问题优化,支持流式写入,低内存占用,无需手动处理 Excel 格式细节 | | MinIO | 8.5.7 | 分布式对象存储服务 | 轻量级、开源、兼容 S3 协议,支持集群部署,适合存储导出的临时文件,可配置文件生命周期,自动清理冗余文件 | | Spring Boot | 2.7.15 | 后端开发框架 | 快速整合各类组件,提供异步任务、依赖注入等核心能力,适配企业级开发 | | Apache Commons IO | 2.13.0 | IO 流工具类 | 简化流的复制、关闭等操作,减少样板代码,提升开发效率 | | ZIP | JDK 内置 | 多 Excel 文件压缩打包 | JDK 原生支持,无需额外引入依赖,可将分批生成的多个 Excel 文件压缩为一个 ZIP 包,方便用户一次性下载 | | Spring Async | Spring Boot 内置 | 异步任务处理 | 将导出任务提交到后台线程池执行,避免前端请求阻塞超时,返回任务 ID 供用户查询进度 |
3. 整体流程架构
已完成
处理中
失败
用户发起导出请求
后端接收请求,生成唯一任务ID
保存任务信息(待处理)到数据库/缓存
提交导出任务到异步线程池
异步线程:分批查询数据库数据(分页)
每批数据通过 EasyExcel 流式生成 Excel 文件
每生成一个 Excel 文件,直接写入 ZIP 流(避免内存堆积)
所有数据处理完成后,关闭 ZIP 流,上传到 MinIO
更新任务信息(完成/失败),记录 MinIO 文件地址与过期时间
用户通过任务ID查询任务状态
任务状态
生成 MinIO 带签名的临时下载 URL
返回当前处理进度(如:已完成50%)
返回失败原因
用户通过 URL 下载压缩包(ZIP)
MinIO 按生命周期策略自动删除过期文件
4. 核心设计亮点
- 分批查询 + 流式写入:每页查询 1000-2000 条数据(可配置),流式写入 Excel,不将所有数据加载到内存,避免 OOM;
- 边生成边压缩:不缓存所有 Excel 文件到内存,而是生成一个 Excel 文件就写入 ZIP 流,进一步降低内存占用;
- 异步任务 + 任务状态跟踪:避免前端请求超时,用户可主动查询任务进度,提升用户体验;
- MinIO 签名 URL 下载:文件不直接返回给前端,而是存储在 MinIO 中,生成带过期时间的签名 URL,保证数据安全;
- 统一样式处理器:封装 Excel 列宽、行高、日期格式等样式,保证导出文件格式统一、美观。
二、前置准备
1. 环境准备
- 部署 MinIO 服务(单机/集群),记录
endpoint(如:http://192.168.1.100:9000)、accessKey、secretKey、存储桶名称(如:excel-export-bucket); - JDK 8 及以上版本;
- Maven 3.6 及以上版本。
2. 依赖引入(完整 pom.xml 片段)
在 Spring Boot 项目中引入以下核心依赖,确保所有功能可正常运行:
`<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.7</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-task</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>`
三、详细实现步骤
第一步:核心配置 类 (MinIO + 异步线程池)
1. MinIO 配置类(配置 + 客户端 Bean + 核心操作服务)
MinIO 是文件存储的核心,先完成配置与核心操作封装,确保文件能正常上传、生成签名 URL。
(1)application.yml 配置 MinIO 信息
`spring:
application:
name: excel-export-demo
task:
execution:
pool:
core-size: 5
max-size: 20
queue-capacity: 100
thread-name-prefix: excel-export-async-
minio:
endpoint: http://192.168.1.100:9000
access-key: minioadmin
secret-key: minioadmin
bucket-name: excel-export-bucket
download-expire-seconds: 3600`
(2)MinIO 配置属性类(绑定 yml 配置)
`package com.example.excelexport.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
private Integer downloadExpireSeconds;
}`
(3)MinIO 客户端 Bean 配置
`package com.example.excelexport.config;
import io.minio.MinioClient;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class MinioConfig {
private final MinioProperties minioProperties;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(minioProperties.getEndpoint())
.credentials(minioProperties.getAccessKey(), minioProperties.getSecretKey())
.build();
}
}`
(4)MinIO 核心操作服务(上传文件 + 生成签名 URL)
`package com.example.excelexport.service;
import com.example.excelexport.config.MinioProperties;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.stat.StatObjectArgs;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class MinioService {
private final MinioClient minioClient;
private final MinioProperties minioProperties;
public String uploadFile(InputStream inputStream, String fileName) {
try {
boolean bucketExists = minioClient.bucketExists(
BucketExistsArgs.builder().bucket(minioProperties.getBucketName()).build()
);
if (!bucketExists) {
minioClient.makeBucket(
MakeBucketArgs.builder().bucket(minioProperties.getBucketName()).build()
);
log.info("MinIO 存储桶 {} 不存在,已自动创建", minioProperties.getBucketName());
}
String objectName = UUID.randomUUID().toString().replace("-", "") + "_" + fileName;
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioProperties.getBucketName())
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.contentType("application/zip")
.build()
);
log.info("文件 {} 上传 MinIO 成功,对象名称:{}", fileName, objectName);
return objectName;
} catch (Exception e) {
log.error("文件上传 MinIO 失败", e);
throw new RuntimeException("文件上传存储服务失败", e);
}
}
public String generateDownloadUrl(String objectName) {
if (!StringUtils.hasText(objectName)) {
throw new IllegalArgumentException("MinIO 对象名称不能为空");
}
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(minioProperties.getBucketName())
.object(objectName)
.build()
);
String url = minioClient.getPresignedObjectUrl(
io.minio.GetPresignedObjectUrlArgs.builder()
.bucket(minioProperties.getBucketName())
.object(objectName)
.method(Method.GET)
.expiry(minioProperties.getDownloadExpireSeconds(), TimeUnit.SECONDS)
.build()
);
return URLDecoder.decode(url, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("生成 MinIO 下载 URL 失败,对象名称:{}", objectName, e);
throw new RuntimeException("生成下载链接失败", e);
}
}
}`
2. 异步线程池配置(可选,优化任务执行)
虽然 Spring 有默认异步线程池,但自定义线程池可更好地 控制 核心线程数、最大线程数,避免任务堆积。
`package com.example.excelexport.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class AsyncExecutorConfig {
@Bean("excelExportExecutor")
public Executor excelExportExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Excel-Export-");
executor.setKeepAliveSeconds(60);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}`
第二步:Excel 核心封装(实体类 + 样式处理器)
1. Excel 导出实体类(带格式注解)
定义导出数据的结构,通过 EasyExcel 注解指定表头、日期格式、列顺序,避免导出格式混乱。
`package com.example.excelexport.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ColumnWidth(25)
@HeadRowHeight(25)
@ContentRowHeight(22)
public class ExportDataVO {
@ExcelProperty(value = "标题", index = 0)
private String title;
@ExcelProperty(value = "内容", index = 1)
private String content;
@ExcelProperty(value = "作者", index = 2)
private String author;
@ExcelProperty(value = "发布时间", index = 3)
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
private Date publishTime;
@ExcelProperty(value = "分类", index = 4)
private String category;
}`
2. Excel 样式处理器(自定义扩展,可选)
虽然 EasyExcel 支持通过注解指定列宽、行高,但对于复杂样式(如表头加粗、数据居中、自定义颜色),可通过 WriteHandler 实现,这里封装两个常用的样式处理器。
(1)列宽自定义处理器(精准控制每列宽度)
`package com.example.excelexport.handler;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
public class CustomColumnWidthHandler implements CellWriteHandler {
private final int[] columnWidths;
public CustomColumnWidthHandler(int[] columnWidths) {
this.columnWidths = columnWidths;
}
@Override
public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, int relativeRowIndex, boolean isHead) {
CellWriteHandler.super.beforeCellCreate(writeSheetHolder, writeTableHolder, cell, relativeRowIndex, isHead);
}
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, int relativeRowIndex, boolean isHead) {
Sheet sheet = writeSheetHolder.getSheet();
Workbook workbook = sheet.getWorkbook();
if (isHead && cell.getColumnIndex() < columnWidths.length) {
sheet.setColumnWidth(cell.getColumnIndex(), columnWidths[cell.getColumnIndex()] * 256);
}
}
}`
(2)表头加粗处理器(提升 Excel 美观度)
`package com.example.excelexport.handler;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Workbook;
public class CustomHeadBoldHandler implements CellWriteHandler {
@Override
public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, int relativeRowIndex, boolean isHead) {
if (isHead) {
Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
CellStyle cellStyle = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
font.setFontHeightInPoints((short) 12);
cellStyle.setFont(font);
cell.setCellStyle(cellStyle);
}
}
}`
第三步:核心业务服务(Excel 导出 + 分批处理 + ZIP 压缩)
这是整个方案的核心,封装普通导出、分批导出、ZIP 压缩的核心逻辑,确保低内存占用、高可用性。
1. 任务状态枚举(辅助跟踪导出任务)
`package com.example.excelexport.enums;
import lombok.Getter;
@Getter
public enum ExportTaskStatusEnum {
PENDING("PENDING", "待处理"),
PROCESSING("PROCESSING", "处理中"),
COMPLETED("COMPLETED", "已完成"),
FAILED("FAILED", "处理失败");
private final String code;
private final String desc;
ExportTaskStatusEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}`
2. 导出任务信息实体(存储任务进度与结果,可持久化到数据库)
`package com.example.excelexport.entity;
import com.example.excelexport.enums.ExportTaskStatusEnum;
import lombok.Data;
import java.util.Date;
@Data
public class ExportTask {
private String taskId;
private String userId;
private String taskName;
private ExportTaskStatusEnum status;
private Integer progress;
private String minioObjectName;
private String errorMsg;
private Date createTime;
private Date completeTime;
}`
3. Excel 导出核心服务(完整实现)
`package com.example.excelexport.service;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.example.excelexport.entity.ExportTask;
import com.example.excelexport.enums.ExportTaskStatusEnum;
import com.example.excelexport.handler.CustomColumnWidthHandler;
import com.example.excelexport.handler.CustomHeadBoldHandler;
import com.example.excelexport.vo.ExportDataVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Slf4j
@Service
@RequiredArgsConstructor
public class ExcelExportCoreService {
private final MinioService minioService;
private static final int PAGE_SIZE = 1000;
private static final int[] COLUMN_WIDTHS = {30, 50, 20, 20, 20};
public void simpleExport(List<ExportDataVO> dataList, OutputStream outputStream) {
if (dataList == null || dataList.isEmpty()) {
log.warn("普通导出数据列表为空,无需执行导出操作");
return;
}
ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream, ExportDataVO.class)
.autoCloseStream(false)
.registerWriteHandler(new CustomColumnWidthHandler(COLUMN_WIDTHS))
.registerWriteHandler(new CustomHeadBoldHandler());
ExcelWriterSheetBuilder sheetBuilder = EasyExcel.writerSheet("数据导出表");
WriteSheet writeSheet = sheetBuilder.build();
writerBuilder.build().write(dataList, writeSheet).finish();
log.info("普通 Excel 导出完成,共导出 {} 条数据", dataList.size());
}
@Async("excelExportExecutor")
public void batchExport(ExportTask task, Long totalCount) {
if (task == null || !StringUtils.hasText(task.getTaskId())) {
throw new IllegalArgumentException("导出任务信息不能为空,且必须包含有效任务 ID");
}
if (totalCount == null || totalCount <= 0) {
log.warn("导出总数据量无效,任务 ID:{}", task.getTaskId());
task.setStatus(ExportTaskStatusEnum.FAILED);
task.setErrorMsg("导出总数据量无效");
task.setCompleteTime(new Date());
return;
}
try {
task.setStatus(ExportTaskStatusEnum.PROCESSING);
task.setProgress(0);
long totalPages = (totalCount + PAGE_SIZE - 1) / PAGE_SIZE;
log.info("大数据量导出任务开始,任务 ID:{},总数据量:{},总分页数:{}", task.getTaskId(), totalCount, totalPages);
try (ByteArrayOutputStream zipByteArrayOut = new ByteArrayOutputStream();
ZipOutputStream zipOut = new ZipOutputStream(zipByteArrayOut)) {
for (int pageNum = 1; pageNum <= totalPages; pageNum++) {
List<ExportDataVO> dataList = queryDataFromDatabase(pageNum, PAGE_SIZE);
ByteArrayInputStream excelInputStream = generateSinglePageExcel(dataList);
ZipEntry zipEntry = new ZipEntry(String.format("数据导出_第%s页.xlsx", pageNum));
zipOut.putNextEntry(zipEntry);
IOUtils.copy(excelInputStream, zipOut);
zipOut.closeEntry();
int progress = (int) ((pageNum * 1.0 / totalPages) * 100);
task.setProgress(progress);
log.info("任务 ID:{},第 {} 页导出完成,当前进度:{}%", task.getTaskId(), pageNum, progress);
excelInputStream.close();
}
zipOut.finish();
InputStream zipInputStream = new ByteArrayInputStream(zipByteArrayOut.toByteArray());
String minioObjectName = minioService.uploadFile(zipInputStream, task.getTaskName() + "_" + new Date().getTime() + ".zip");
task.setStatus(ExportTaskStatusEnum.COMPLETED);
task.setProgress(100);
task.setMinioObjectName(minioObjectName);
task.setCompleteTime(new Date());
log.info("大数据量导出任务完成,任务 ID:{},MinIO 对象名称:{}", task.getTaskId(), minioObjectName);
} catch (Exception e) {
log.error("任务 ID:{},ZIP 压缩或上传 MinIO 失败", task.getTaskId(), e);
task.setStatus(ExportTaskStatusEnum.FAILED);
task.setErrorMsg("文件压缩或存储失败:" + e.getMessage());
task.setCompleteTime(new Date());
}
} catch (Exception e) {
log.error("任务 ID:{},大数据量导出任务异常", task.getTaskId(), e);
task.setStatus(ExportTaskStatusEnum.FAILED);
task.setErrorMsg("导出任务执行异常:" + e.getMessage());
task.setCompleteTime(new Date());
}
}
private ByteArrayInputStream generateSinglePageExcel(List<ExportDataVO> dataList) {
if (dataList == null || dataList.isEmpty()) {
return new ByteArrayInputStream(new byte[0]);
}
try (ByteArrayOutputStream excelOut = new ByteArrayOutputStream()) {
simpleExport(dataList, excelOut);
return new ByteArrayInputStream(excelOut.toByteArray());
} catch (Exception e) {
log.error("生成单页 Excel 失败", e);
throw new RuntimeException("生成单页 Excel 数据失败", e);
}
}
private List<ExportDataVO> queryDataFromDatabase(int pageNum, int pageSize) {
List<ExportDataVO> dataList = new ArrayList<>();
Date now = new Date();
for (int i = 0; i < pageSize; i++) {
ExportDataVO vo = new ExportDataVO();
vo.setTitle("测试标题_" + (pageNum - 1) * pageSize + i);
vo.setContent("测试内容_" + (pageNum - 1) * pageSize + i);
vo.setAuthor("测试作者");
vo.setPublishTime(now);
vo.setCategory("测试分类");
dataList.add(vo);
}
return dataList;
}
}`
第四步:控制层(暴露接口,接收请求,返回任务 ID/下载 URL)
封装前端可调用的 REST 接口,包括发起导出任务、查询任务状态、获取下载 URL三个核心接口。
`package com.example.excelexport.controller;
import com.example.excelexport.entity.ExportTask;
import com.example.excelexport.enums.ExportTaskStatusEnum;
import com.example.excelexport.service.ExcelExportCoreService;
import com.example.excelexport.service.MinioService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@RestController
@RequestMapping("/api/excel/export")
@RequiredArgsConstructor
public class ExcelExportController {
private final ExcelExportCoreService excelExportCoreService;
private final MinioService minioService;
private final Map<String, ExportTask> taskMap = new ConcurrentHashMap<>();
@PostMapping("/submit")
public ResponseEntity<Map<String, Object>> submitExportTask(
@RequestParam String userId,
@RequestParam String taskName,
@RequestParam Long totalCount) {
String taskId = UUID.randomUUID().toString().replace("-", "");
ExportTask task = new ExportTask();
task.setTaskId(taskId);
task.setUserId(userId);
task.setTaskName(taskName);
task.setStatus(ExportTaskStatusEnum.PENDING);
task.setProgress(0);
task.setCreateTime(new Date());
taskMap.put(taskId, task);
excelExportCoreService.batchExport(task, totalCount);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "导出任务已提交");
result.put("data", taskId);
return new ResponseEntity<>(result, HttpStatus.OK);
}
@GetMapping("/status/{taskId}")
public ResponseEntity<Map<String, Object>> queryTaskStatus(@PathVariable String taskId) {
Map<String, Object> result = new HashMap<>();
if (!taskMap.containsKey(taskId)) {
result.put("code", 404);
result.put("msg", "任务 ID 不存在");
return new ResponseEntity<>(result, HttpStatus.NOT_FOUND);
}
ExportTask task = taskMap.get(taskId);
result.put("code", 200);
result.put("msg", "查询成功");
Map<String, Object> data = new HashMap<>();
data.put("taskId", task.getTaskId());
data.put("taskName", task.getTaskName());
data.put("status", task.getStatus().getDesc());
data.put("progress", task.getProgress() + "%");
data.put("createTime", task.getCreateTime());
if (ExportTaskStatusEnum.COMPLETED.equals(task.getStatus())) {
String downloadUrl = minioService.generateDownloadUrl(task.getMinioObjectName());
data.put("downloadUrl", downloadUrl);
} else if (ExportTaskStatusEnum.FAILED.equals(task.getStatus())) {
data.put("errorMsg", task.getErrorMsg());
}
result.put("data", data);
return new ResponseEntity<>(result, HttpStatus.OK);
}
@GetMapping("/simple")
public void simpleExport(HttpServletResponse response) {
try {
List<ExportDataVO> dataList = new ArrayList<>();
Date now = new Date();
for (int i = 0; i < 100; i++) {
ExportDataVO vo = new ExportDataVO();
vo.setTitle("简单导出标题_" + i);
vo.setContent("简单导出内容_" + i);
vo.setAuthor("简单导出作者");
vo.setPublishTime(now);
vo.setCategory("简单导出分类");
dataList.add(vo);
}
String fileName = URLEncoder.encode("小数据量测试导出.xlsx", "UTF-8");
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
response.setCharacterEncoding("UTF-8");
excelExportCoreService.simpleExport(dataList, response.getOutputStream());
} catch (Exception e) {
log.error("小数据量直接导出失败", e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
}`
四、完整代码示例(可直接运行)
1. 项目结构(Spring Boot 项目)
`excel-export-demo/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── excelexport/
│ │ │ ├── ExcelExportDemoApplication.java // 启动类
│ │ │ ├── config/ // 配置类
│ │ │ │ ├── AsyncExecutorConfig.java
│ │ │ │ ├── MinioConfig.java
│ │ │ │ └── MinioProperties.java
│ │ │ ├── controller/ // 控制层
│ │ │ │ └── ExcelExportController.java
│ │ │ ├── entity/ // 实体类
│ │ │ │ └── ExportTask.java
│ │ │ ├── enums/ // 枚举类
│ │ │ │ └── ExportTaskStatusEnum.java
│ │ │ ├── handler/ // 样式处理器
│ │ │ │ ├── CustomColumnWidthHandler.java
│ │ │ │ └── CustomHeadBoldHandler.java
│ │ │ ├── service/ // 服务层
│ │ │ │ ├── ExcelExportCoreService.java
│ │ │ │ └── MinioService.java
│ │ │ └── vo/ // 导出 VO
│ │ │ └── ExportDataVO.java
│ │ └── resources/
│ │ └── application.yml // 配置文件
│ └── test/
│ └── java/
└── pom.xml // 依赖配置`
2. 启动类(ExcelExportDemoApplication.java)
`package com.example.excelexport;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ExcelExportDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ExcelExportDemoApplication.class, args);
log.info("Excel 导出 demo 项目启动成功");
}
}`
3. 完整可运行说明
- 按照上述项目结构创建 Spring Boot 项目,复制所有代码;
- 修改
application.yml中的 MinIO 配置(endpoint、accessKey、secretKey); - 在 MinIO 中提前创建配置的存储桶(如:
excel-export-bucket); - 启动项目,访问以下接口进行测试:
- 小数据量直接导出:
http://localhost:8080/api/excel/export/simple(直接下载 Excel 文件); - 提交大数据量导出任务:
POST http://localhost:8080/api/excel/export/submit?userId=test001&taskName=百万数据导出&totalCount=100000(返回任务 ID); - 查询任务状态:
GET http://localhost:8080/api/excel/export/status/{taskId}(任务完成后返回下载 URL);
- 小数据量直接导出:
- 访问下载 URL,即可下载压缩后的 ZIP 包(包含多个 Excel 文件)。
五、注意事项与最佳实践
- 分页查询优化:大数据量分页查询时,避免使用
OFFSET/LIMIT(MySQL 中OFFSET过大时性能低下),建议使用「主键ID分段查询」(如:where id > lastId limit 1000); - 文件生命周期管理:在 MinIO 中配置存储桶的生命周期策略(如:7 天自动删除),避免冗余文件占用存储空间;
- 任务超时处理:对于异步任务,可配置超时时间,超时未完成的任务自动标记为失败;
- 权限控制:除了 MinIO 签名 URL,还可在接口层增加用户权限校验(如:JWT 令牌),确保只有任务发起者才能查询和下载;
- 异常重试:对于分页查询失败、Excel 生成失败的场景,可增加重试机制(如:最多重试 3 次),提升任务成功率;
- 内存监控:部署后监控服务器内存使用情况,根据实际情况调整
PAGE_SIZE和异步线程池参数; - 中文文件名处理:所有涉及文件名的地方,必须使用
URLEncoder编码,避免中文乱码。
六、方案总结
本文介绍的 EasyExcel + MinIO + ZIP 大数据量 Excel 导出方案,完美解决了传统导出的 OOM、超时等问题,具备以下核心优势:
- 高性能:EasyExcel 流式写入,低内存占用,支持百万级数据导出;
- 高可用:异步处理,避免前端请求超时,任务状态可跟踪;
- 高安全:MinIO 签名 URL 下载,文件自动过期,数据安全有保障;
- 高复用:代码模块化封装,可直接复用在企业级项目中;
- 优体验:多文件压缩打包,用户一次性下载,提升使用体验。
该方案已在多个企业级项目中落地,可适配电商、政务、金融等多个行业的大数据量 Excel 导出需求,具有较强的实用性和扩展性。