
文中不仅会详解导入导出的核心步骤:包括依赖配置、实体类注解映射、监听器使用(解决导入数据逐行处理),还会结合实际场景提供实用方案——比如数据导入时的表头校验、必填项验证,导出时的单元格样式定制(字体、颜色、列宽设置),以及大数据量下的分批读写优化。针对开发中易踩的坑,我们也整理了避坑指南:如日期格式自动转换异常、合并单元格数据读取不全、导出文件名称乱码等问题的解决方案。
更重要的是,文中提供完整可复用的代码示例,覆盖单sheet导入导出、多sheet处理、动态表头生成等常见场景,帮助开发者跳过试错环节,直接落地实践。无论你是刚接触Excel处理的新手,还是想优化现有功能的资深开发者,都能通过本文快速上手,轻松应对各类Excel业务需求。
你是不是也遇到过这种情况?辛辛苦苦写的Java项目,一到Excel导入导出功能就头大——用POI处理2万行数据内存直接飙到500MB,服务器报警不停;导出个带格式的报表,调个单元格颜色能折腾一下午;好不容易上线了,用户又反馈”导入时日期变成了数字””合并单元格的数据没读全”……说实话,我之前帮朋友的电商项目做订单数据导出时,就踩过这些坑,当时用POI硬扛,结果线上OOM(内存溢出)三次,被运维追着改了一周。直到后来换成阿里开源的EasyExcel,这些问题才算彻底解决。今天就掏心窝子跟你聊聊,怎么用EasyExcel把Excel导入导出做得又快又稳,再把那些踩过的坑一个个给你填平。
EasyExcel基础实战:从依赖配置到核心实现
为啥非要选EasyExcel?这得从传统POI的”痛点三连”说起。去年我接手一个政府项目的Excel导入功能,原代码用POI做的,用户上传5万行数据就卡成PPT,后台日志疯狂刷”OutOfMemoryError”。后来查内存快照发现,POI把整个Excel文件都加载到内存里,5万行×20列的数据直接占了600MB内存——服务器就给了1G内存,不崩才怪。而EasyExcel最牛的地方,就是它采用”一行一行解析”的策略,解析时不会一次性加载所有数据,而是通过SAX解析器逐行读取,解析完立即释放内存。我当时把项目换成EasyExcel后,同样5万行数据,内存占用直接降到50MB,处理速度从3分钟缩到20秒,甲方当场给我加了鸡腿。
第一步:把EasyExcel请进项目
想用EasyExcel,先得把依赖配好。如果你的项目用Maven,直接在pom.xml里加这段:
com.alibaba
easyexcel
3.3.0 <!-
用最新稳定版,可去Maven仓库查 >
要是Gradle项目,就加implementation 'com.alibaba:easyexcel:3.3.0'
。这里提醒一句,别同时引POI和EasyExcel的旧版本,容易冲突——我之前有个实习生就犯过这错,引了POI 3.17和EasyExcel 2.2.6,结果启动就报类冲突,后来把POI exclude掉才好。
第二步:搞懂核心概念,别被”注解”绕晕
EasyExcel的核心就俩东西:实体类注解和监听器。实体类注解是用来映射Excel列和Java字段的,比如Excel里的”订单编号”列,对应Java实体的orderNo
字段,就靠@ExcelProperty
注解关联。举个例子,你要导出订单数据,实体类可以这么写:
@Data
public class OrderExcelDTO {
@ExcelProperty("订单编号") // Excel表头名称
private String orderNo;
@ExcelProperty(value = "订单金额", converter = MoneyConverter.class) // 自定义转换器
private BigDecimal amount;
@ExcelProperty("创建时间")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss") // 日期格式
private Date createTime;
}
这里的@DateTimeFormat
和自定义转换器很重要,后面避坑部分会细说。
另一个核心是监听器,主要用在导入功能。为啥需要监听器?因为导入时Excel数据是一行一行解析的,监听器就是用来”接住”每行数据并处理的——比如校验、入库。你可以理解成:EasyExcel是快递员,每行数据是包裹,监听器就是你家的收件箱,快递员把包裹一个个放进收件箱,你再从收件箱里拿出来处理。
第三步:基础导入导出代码,30行搞定
先看导出,比如导出订单列表到Excel,核心代码就这几行:
// 导出接口示例
@GetMapping("/exportOrder")
public void exportOrder(HttpServletResponse response) throws IOException {
//
设置响应头,解决文件名乱码(后面避坑会讲细节)
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");
//
查询数据(实际项目从数据库查,这里模拟10万行数据)
List orderList = orderService.listOrderForExport();
//
EasyExcel写出数据
EasyExcel.write(response.getOutputStream(), OrderExcelDTO.class)
.sheet("订单列表") // sheet名称
.doWrite(orderList); // 写入数据
}
是不是比POI简单多了?不用自己创建Workbook、Sheet、Row这些对象,EasyExcel全帮你封装好了。
再看导入,比如导入商品数据,需要定义监听器:
// 自定义监听器
public class ProductImportListener extends AnalysisEventListener {
private final ProductService productService;
private List dataList = new ArrayList(1000); // 批量处理,每1000条存一次库
public ProductImportListener(ProductService productService) {
this.productService = productService;
}
// 每行数据解析完成后调用
@Override
public void invoke(ProductExcelDTO data, AnalysisContext context) {
//
数据校验(比如必填项、格式校验)
if (StringUtils.isEmpty(data.getProductName())) {
throw new BusinessException("商品名称不能为空");
}
//
加入临时列表
dataList.add(data);
//
满1000条批量入库,避免频繁操作数据库
if (dataList.size() >= 1000) {
productService.batchSave(dataList);
dataList.clear();
}
}
// 所有数据解析完成后调用(收尾工作)
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
if (!dataList.isEmpty()) {
productService.batchSave(dataList); // 保存剩余数据
}
}
}
然后在Controller里调用:
@PostMapping("/importProduct")
public String importProduct(@RequestParam("file") MultipartFile file) throws IOException {
// 传入监听器和业务服务,开始导入
EasyExcel.read(file.getInputStream(), ProductExcelDTO.class, new ProductImportListener(productService))
.sheet() // 读取第一个sheet
.doRead();
return "导入成功";
}
这就是最基础的导入导出流程,亲测不管是简单的列表导出,还是带校验的导入,都能搞定。
进阶场景处理与避坑全指南
基础功能会了,但实际开发中需求往往更复杂:比如要导出100万行数据怎么办?Excel里有多个sheet怎么处理?动态表头(比如每月报表表头不一样)怎么生成?更头疼的是那些”隐形坑”——明明代码没错,结果日期变成数字、合并单元格数据读不全、文件名在IE里乱码……别慌,这些我都帮你踩过了,一个个说清楚。
大数据量与多sheet处理:从”卡爆”到”丝滑”
先说大数据量导出,比如导出100万行订单数据。如果直接用上面的基础代码,虽然内存不会OOM,但一次性查100万条数据可能把数据库查崩。正确做法是分批查询+分批写出。我之前帮一个物流项目做过,用MyBatis的RowBounds
或者分页插件,每次查1万行,查完一批写一批:
// 大数据量分批导出示例
public void exportBigData(HttpServletResponse response) throws IOException {
// 设置响应头(略)
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), OrderExcelDTO.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("订单数据").build();
int pageNum = 1;
int pageSize = 10000;
while (true) {
// 分批查询数据
List dataList = orderService.listByPage(pageNum, pageSize);
if (CollectionUtils.isEmpty(dataList)) {
break; // 没有数据了,退出循环
}
// 分批写出
excelWriter.write(dataList, writeSheet);
pageNum++;
}
// 记得关闭流
excelWriter.finish();
}
这样数据库压力小,内存占用也稳定,100万行数据导出完全不卡顿。
再看多sheet导出,比如一个Excel里有”订单数据”和”商品数据”两个sheet。EasyExcel支持创建多个WriteSheet
对象,分别写入:
// 多sheet导出示例
public void exportMultiSheet(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String fileName = URLEncoder.encode("多sheet数据", "UTF-8").replaceAll("+", "%20");
response.setHeader("Content-disposition", "attachment;filename=utf-8''" + fileName + ".xlsx");
ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).build();
// 第一个sheet:订单数据
WriteSheet orderSheet = EasyExcel.writerSheet(0, "订单数据").head(OrderExcelDTO.class).build();
excelWriter.write(orderService.listOrderForExport(), orderSheet);
// 第二个sheet:商品数据
WriteSheet productSheet = EasyExcel.writerSheet(1, "商品数据").head(ProductExcelDTO.class).build();
excelWriter.write(productService.listProductForExport(), productSheet);
excelWriter.finish();
}
是不是很简单?Sheet索引从0开始,还能自定义sheet名称。
避坑指南:这5个坑90%的人都会踩
坑1:日期格式自动转成数字
表现
:导出的Excel里,日期字段显示成”45231″这种数字(其实是Excel的日期序列号)。 原因:EasyExcel默认会把Date
类型字段转成Excel序列号,而不是格式化的日期字符串。 解决:在实体类字段上加上@DateTimeFormat
注解指定格式,比如@DateTimeFormat("yyyy-MM-dd HH:mm:ss")
,或者用@JsonFormat
(但@DateTimeFormat
更推荐,专门用于Excel)。
坑2:合并单元格数据只读取第一行
表现
:导入带合并单元格的Excel(比如A1-A3合并),结果只读到A1的数据,A2、A3都是null。 原因:EasyExcel默认解析合并单元格时,只有第一行有数据,其他行视为空。 解决:自定义合并单元格监听器,记录合并单元格的范围,填充空数据。具体可以参考EasyExcel官方文档的合并单元格处理示例,核心是通过AnalysisContext
获取合并单元格信息,然后在invoke
方法里补全数据。
坑3:导出文件名在IE浏览器乱码
表现
:Chrome浏览器下载文件名正常,IE里变成”???xlsx”。 原因:不同浏览器对文件名编码的处理方式不同,IE需要用UTF-8编码+特殊格式。 解决:用下面这段代码设置响应头,亲测兼容所有浏览器:
String fileName = URLEncoder.encode("订单数据", "UTF-8").replaceAll("+", "%20");
response.setHeader("Content-disposition", "attachment;filename=utf-8''" + fileName + ".xlsx");
关键是filename=utf-8''
这个格式,IE能正确识别。
坑4:导入时表头和实体类不匹配
表现
:导入报错”Can not find ‘XXX’ in head”,或者数据对应错误。 原因:Excel表头名称和实体类@ExcelProperty
的值不一致,或者顺序不对。 解决:有两种办法:① 严格保证Excel表头和@ExcelProperty
的值完全一致(包括空格、标点);② 用@ExcelProperty(index = 0)
指定列索引(0表示第一列),这样表头名称可以不对应,但顺序要对。
坑5:大数据量导出时CPU占用过高
表现
:导出100万行数据时,服务器CPU飙升到100%,处理缓慢。 原因:单线程处理大量数据,CPU忙不过来。 解决:用线程池异步导出,把任务丢到线程池里执行,避免阻塞主线程。代码示例:
// 配置线程池
@Bean
public ThreadPoolTaskExecutor excelExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.initialize();
return executor;
}
// 异步导出
@Async("excelExecutor")
public CompletableFuture asyncExportOrder(...) {
// 导出逻辑(和前面一样)
return CompletableFuture.runAsync(() -> { ... });
}
性能优化:让你的Excel处理快3倍
除了上面的分批读写和异步处理,再分享3个实战优化技巧:
HashMap
里,一行代码搞定映射,我之前这么改完,导入速度直接快了2倍。 excelType(ExcelTypeEnum.XLSX)
(默认就是XLSX),比XLS格式(Excel 2003版)处理速度快30%,而且支持更大数据量(XLS最多65536行,XLSX无限制)。 EasyExcel.write().autoCloseStream(false)
关闭自动关流,避免频繁打开关闭流浪费性能。 最后给你一个小 写Excel功能时,一定要先写单元测试!比如测试10万行数据导入会不会OOM,不同格式的Excel能不能正常解析。我之前就因为没测合并单元格的情况,上线后被用户反馈数据丢失,连夜回公司改代码——血的教训啊!
如果你按这些方法试了,或者遇到了其他搞不定的坑,欢迎在评论区告诉我,咱们一起把Excel处理这块硬骨头啃下来!
其实要说EasyExcel和POI的区别,最直观的感受就是“内存占用”和“写代码的麻烦程度”。我之前帮一个做ERP系统的朋友看代码,他用POI导10万行销售数据,服务器内存直接从300MB飙到800MB,差点触发OOM告警——后来一查,POI是把整个Excel文件像“打包”一样全塞进内存里,相当于你把一整箱文件全堆在桌上,占地方不说,找东西还费劲。但EasyExcel不一样,它用的是SAX解析器,就像流水线一样,解析一行数据就处理一行,处理完就扔掉,不占内存。同样10万行数据,换成EasyExcel后,内存占用稳定在60MB左右,服务器风扇都安静多了。
再说说写代码的体验,简直是“手动挡”和“自动挡”的区别。用POI写导出,你得自己创建Workbook、Sheet、Row、Cell,调各种样式还得一步步设置字体、颜色、边框,光创建一个带表头的Sheet就得写20多行代码。我之前带实习生做项目,他用POI写个简单的用户列表导出,光处理表头和单元格格式就折腾了一下午。但EasyExcel把这些都封装好了,你定义个DTO类,加几个@ExcelProperty注解指定表头,一行代码就能把数据写到Excel里,根本不用管底层的Workbook怎么创建。当然啦,如果只是处理几百行的小表格,POI也不是不能用,毕竟不用额外引依赖;但要是数据超过1万行,或者需要调格式、做校验,选EasyExcel能少熬好几个夜。
EasyExcel和POI有什么核心区别?应该怎么选?
EasyExcel和POI的核心区别在于内存占用和易用性。POI会将整个Excel文件加载到内存,处理10万行数据可能占用数百MB内存,容易OOM;而EasyExcel采用SAX逐行解析,内存占用低(通常仅需几十MB),且API更简洁,无需手动创建Workbook、Sheet等对象。如果是简单的小数据量处理,POI也能用;但涉及大数据量(超过1万行)、格式复杂或追求开发效率时,优先选EasyExcel。
用EasyExcel处理100万行数据时,如何避免内存溢出?
处理100万行数据需做好“分批读写+内存控制”。导入时,通过监听器(AnalysisEventListener)逐行接收数据,积累到1000-5000行时批量入库,避免数据堆积在内存;导出时,用ExcelWriter分批查询数据(如每次查1万行),分批写入Excel,写完一批释放资源。同时避免在循环中创建大量对象,可复用List或DTO对象,进一步降低内存占用。
动态表头(如每月报表表头不同)怎么用EasyExcel实现?
动态表头需放弃固定实体类映射,改用List>定义表头结构。 表头为“日期、销售额、利润”时,先创建表头列表:List> head = Arrays.asList(Arrays.asList(“日期”), Arrays.asList(“销售额”), Arrays.asList(“利润”));然后在EasyExcel.write时,不指定head参数(或设为null),通过writerSheet().head(head)动态传入表头。导出数据时,用List>存储每行数据,与表头列数对应即可。
导入Excel时,如何校验必填项和格式(如手机号、邮箱)?
数据校验可在监听器的invoke方法中实现。当EasyExcel解析完一行数据后,会调用invoke方法,此时可对DTO对象进行校验:① 必填项校验:判断关键字段(如订单号、用户名)是否为空,为空则抛出异常或记录错误信息;② 格式校验:用正则表达式验证手机号(如匹配^1[3-9]d{9}$)、邮箱(如匹配^w+([-+.]w+)@w+([-.]w+).w+([-.]w+)*$),不通过则标记为无效数据。校验失败的数据可单独收集,导入完成后返回给用户。
导出Excel时,如何自定义单元格样式(如字体颜色、列宽)?
EasyExcel通过WriteHandler接口实现样式定制。例如设置列宽:创建自定义handler继承AbstractColumnWidthStyleStrategy,重写setColumnWidth方法指定列宽;设置字体颜色:继承AbstractCellStyleStrategy,在setContentCellStyle方法中通过CellStyle设置字体颜色(如cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex()))。定义好handler后,在EasyExcel.write时通过.registerWriteHandler(handler)注册即可生效。常用样式包括列宽、字体大小、背景色、边框等,均可通过handler实现。