Spring Boot项目导入大量Excel数据的解决方案
需求分析
工作的时候出现一个比较特别的需求:一次性导入16万行有着17个字段的Excel表格,原本我维护的代码使用ExcelUtil实现的,但是客户使用的时候反馈:一次性导入1万条数据会直接卡住,并且数据一直增加。
后来查看日志、自己在Windows上操作一遍,寻找相关关键词,很可能是发生了内存溢出。
最后也在ExcelUtil的官方文档中得到了进一步证实:
尝试解决
流方式读取?
一开始想着用最小的改动来完成这个变态需求,后来在写的时候才发现ExcelUtil的流方式虽然能够支持大量数据的读取但是也相应地有个致命缺点:封装性比较差!很难和Web业务结合,所以我果断选择跑路。
EasyExcel!
在网上寻找大量数据导入Excel的方法中,发现了一个神奇宝贝:EasyExcel!
什么是Easy Excel?
EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。
他能让你在不用考虑性能、内存的等因素的情况下,快速完成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模式,在上层做了模型转换的封装,让使用者更加简单方便
怎么用?
说句实话,看官方文档的例子我苦恼了很久
导入的逻辑都是在Listener中实现的,关于导入的结果应该怎么体现呢,这让我挺困惑的,搜网一圈之后发现一篇类似的issue:
- 自行在构造器传入一些变量来满足业务
- 自行插入些成员变量来获得导入情况
根据这个思路,我写了这个Listener
@Slf4j
public class DataListener extends AnalysisEventListener<ImportParam> {
/**
* 每隔100条存储数据库,然后清理list,方便内存回收
*/
private static final int BATCH_COUNT = 100;
private List<Temp> list = new ArrayList<>();
// 记录成功条数
@Getter
private int successCount;
// 记录错误信息
@Getter
private List<String> errorCount = new ArrayList<>();
private XXXService xxxService;
private HttpServletRequest request; // 根据自己业务需求传
public DataListener(XXXService xxxService, HttpServletRequest request) {
this.xxxService = xxxService;
this.request = request;
}
@Override
public void invoke(ImportParam tempValue, AnalysisContext context ) {
// TODO
// 导入数据字段检验
if (tempValue.getName() == null || Tools.isEmpty(tempValue.getName().toString()) || Tools.isEmpty(tempValue.getName().toString())) {
errorCount.add("导入数据必填项为空");
return ;
}
...
// TODO
// 这里模拟ImportParam二次封装成多个不同对象的过程,Service可能操作多个表
tempValue...>add
log.info("准备导入{}的数据:data = {}",add.getDataA().getName(), add.getDataA());
list.add(add);
if (list.size() >= BATCH_COUNT) {
saveData(request, 其他参数);
list.clear();
this.successCount+=100;
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// TODO
// 对剩下不足100的数据进行处理
...
saveData(phoneArray, list, adminId, request);
// 记录成功导入的数量
this.successCount += list.size();
}
public void saveData(HttpServletRequest request, 其他参数){
try {
Map<String, String> result = xxxService.daoforeachlist(其他参数, request);
} catch (Exception e) {
throw new RuntimeException(e);
}
};
}
控制层的写法:
@ApiOperation(value = "Excel导入")
@PostMapping(value = "/import")
public Result api_import_client(MultipartFile file, HttpServletRequest request)throws XXXBusinessException,Exception{
try {
if (EmptyUtils.isEmpty(file)) {
return new Result("0", "请传入文件");
}
init(request);
InputStream inputStream = file.getInputStream();
// 构建监听器实例,传入EasyExcel、文件输入流导入数据
DataListener dataListener = new DataListener(clientService, operatorAdminId, adminId, request);
EasyExcel.read(inputStream, ImportParam.class, dataListener).sheet().doRead();
// 通过监听器实例获取状态
List<String> errorCount = dataListener.getErrorCount();
int successCount = dataListener.getSuccessCount();
// 返回导入结果
if (errorCount.isEmpty()) {
return new Result<>("1", "成功操作了"+successCount+"条记录");
} else {
log.error("出错记录:\n" + errorCount.toString());
return new Result("0", "成功操作了"+successCount+"条记录,"+errorCount.size()+"条记录出错");
}
} catch (Exception e) {
e.printStackTrace(); return new Result("0", "系统出错,操作失败");
}
}
EasyExcel 怎么做到“再大的excel也不会出现内存溢出”的呢?
我的理解是:
EasyExcel 的设计挺巧妙的,把Listener设计成自己执行,也可以自行在构造器传入一些变量来满足业务,最重要的是可以自定义BATCH_COUNT,通过每次循环中判断列表数据是否超过了BATCH_COUNT来控制list不会过大,list不会过大,一点一点存,自然不会有内存溢出的问题了,“再大的excel也就不会出现内存溢出”~只是相当于也许会多花点时间(实际上EasyExcel比ExcelUtil快)
参与讨论
(Participate in the discussion)
参与讨论