目录
介绍
EasyExcel 是阿里巴巴开源的一个基于 Java 的简单的、节省内存的 Excel 处理工具, Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。 easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。
本篇主要讲的是 EasyExcel 的导出功能,即写入功能,且分为单次写入和多次写入,想要了解更多可参考官方网站:EasyExcel官方网站。
快速使用
- 依赖引入
XML
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
- 示例代码
java
/**
*
* 结合官方提供资料完成快速开始
* 文件下载(失败了会返回一个有部分数据的Excel)
* <p>
* 1. 创建excel对应的实体对象
* <p>
* 2. 设置返回的 参数
* <p>
* 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
*/
@GetMapping("/ff/export")
public void exportExcel(HttpServletResponse response) throws IOException {
// 设置响应格式(Excel)
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
// 处理文件名中文乱码
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
// 写入数据
EasyExcel.write(response.getOutputStream(), User.class)
.sheet("测试用户列表")
.doWrite(getExportData());
}
/**
* 生成导出的模拟数据
* @return
*/
public List<User> getExportData() {
List<User> userList = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
User user = new User();
user.setName("用户" + i);
user.setAge(20 + new Random().nextInt(30)); // 年龄20-50岁
user.setEmail("user" + i + "@XX.com");
userList.add(user);
}
return userList;
}
/**
*
* Excel映射实体
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "姓名", index = 0)
private String name;
@ExcelProperty(value = "年龄", index = 1)
private Integer age;
@ExcelProperty(value = "邮箱", index = 2)
private String email;
}
分页查询并导出
官网参照
重复多次写入(写到单个或者多个 Sheet)中
java
/**
* 重复多次写入
* <p>
* 1. 创建excel对应的实体对象 参照{@link ComplexHeadData}
* <p>
* 2. 使用{@link ExcelProperty}注解指定复杂的头
* <p>
* 3. 直接调用二次写入即可
*/
@Test
public void repeatedWrite() {
// 方法1: 如果写到同一个sheet
String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
// 这里注意 如果同一个sheet只要创建一次
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
for (int i = 0; i < 5; i++) {
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
}
// 方法2: 如果写到不同的sheet 同一个对象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
}
// 方法3 如果写到不同的sheet 不同的对象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
// 实际上可以一直变
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(DemoData.class).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
}
}
分页导出
- 模拟数据导出

- 示例代码(主要是实现写到不同的sheet 同一个对象这种方式)
java
package com.ffliuli.business;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.ffliuli.entity.User;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
*
* 分页读取数据可避免OOM的发生
*/
@Service
public class BigDataExportBusiness {
private static final int MAX_ROWS_SHEET = 1000000;
private static final int PAGE_SIZE = 3000;
private static final long TOTAL_DATA_COUNT = 3000000;
/**
* 导出
*/
public void exportBigData(HttpServletResponse response) throws IOException {
setExcelResponseHeader(response, "300万用户列表测试");
try (OutputStream outputStream = response.getOutputStream()) {
// 初始化ExcelWriter,绑定输出流和数据模型
ExcelWriter excelWriter = EasyExcel.write(outputStream, User.class).build();
// 初始化Sheet参数,Sheet序号
int currentSheetNo = 0;
// 当前Sheet已写入行数
int currentSheetRowCount = 0;
WriteSheet currentWriteSheet = createWriteSheet(currentSheetNo, "用户列表-" + (currentSheetNo + 1));
int pageNum = 1;
while (true) {
List<User> pageDataList = getPageUserData(pageNum, PAGE_SIZE);
if (CollectionUtils.isEmpty(pageDataList)) {
break;
}
// 检查当前Sheet是否已满,满则创建新Sheet
if (currentSheetRowCount + pageDataList.size() > MAX_ROWS_SHEET) {
currentSheetNo++;
currentWriteSheet = createWriteSheet(currentSheetNo, "用户列表-" + (currentSheetNo + 1));
currentSheetRowCount = 0;
}
excelWriter.write(pageDataList, currentWriteSheet);
currentSheetRowCount += pageDataList.size();
// 进入下一页查询
pageNum++;
}
// 完成写入并释放资源
excelWriter.finish();
}
}
/**
*
* 设置Excel下载响应头
*/
private void setExcelResponseHeader(HttpServletResponse response, String fileName) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
.replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
}
/**
*
* 创建新的WriteSheet
*/
private WriteSheet createWriteSheet(int sheetNo, String sheetName) {
return EasyExcel.writerSheet(sheetNo, sheetName).build();
}
/**
*
* 模拟数据库数据
*/
private List<User> getPageUserData(int pageNum, int pageSize) {
List<User> userList = new ArrayList<>(pageSize);
Random random = new Random();
// 计算当前页数据的起止索引
long startIndex = (pageNum - 1L) * pageSize + 1;
long endIndex = Math.min(startIndex + pageSize - 1, TOTAL_DATA_COUNT);
if (startIndex > TOTAL_DATA_COUNT) {
return userList;
}
// 生成当前页模拟数据
for (long i = startIndex; i <= endIndex; i++) {
User user = new User();
user.setName("用户" + i);
user.setAge(20 + random.nextInt(30));
user.setEmail("user" + i + "@XX.com");
userList.add(user);
}
return userList;
}
}
// 以上代码仅仅是示例代码可对其进行扩展,如加上前端请求时,传输过来的条件进行分页查询后再写入excel进而导出。
// 要使其更加贴合实战项目需进行升级改造,可从不同场景进行切入。
内存使用情况:

结尾
总之,基于 EasyExcel 实现 Excel 导出功能的技术分享就完整收尾了。本文从 EasyExcel 的核心价值切入 ------ 相较于 Apache POI、jxl 等传统框架,它通过重写 07 版 Excel 解析逻辑,将 3M 文件的内存占用从 100M 级降至几 M,彻底解决了大数据导出的 OOM 痛点;随后逐步落地实战:从引入 3.1.1 版本依赖、定义带@ExcelProperty注解的 User 映射实体,到编写单次导出接口(含响应格式配置、中文文件名编码处理),再到深入分页导出的三种核心场景(同 Sheet 多次写入、不同 Sheet 同对象写入、不同 Sheet 不同对象写入),最后通过 300 万用户数据导出的案例,演示了如何用 "分页查询(每页 3000 条)+ 单 Sheet 百万行上限拆分" 实现高效稳定的大数据导出,且从监控数据可见,节省堆内存,CPU 消耗极低。
在实际项目中落地时,建议根据数据量级灵活选择方案:千级以内小数据量可直接用单次写入简化代码;百万级大数据务必优先采用分页查询 + 多 Sheet 拆分的模式,同时保留setExcelResponseHeader中的缓存控制配置,避免浏览器下载异常。如果遇到复杂表头、单元格合并、数据格式自定义等场景,或者对文中分页逻辑有更优优化思路,欢迎在评论区留言交流 。
读者后续可以继续拓展 EasyExcel 的相关内容,比如 Excel 导入功能、异常重试机制、复杂模板填充等实战场景。若本文对你的开发有帮助,不妨点赞收藏。感谢你的耐心阅读,技术之路漫长,同行者常伴,期待与你下次交流~~~