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中实现的,如果想返回成功多少失败多少条,应该怎么返回?
- 自行在构造器传入一些变量来满足业务
- 自行插入些成员变量来获得导入情况
根据这个思路,我写了这个Listener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
@Slf4j public class DataListener extends AnalysisEventListener<ImportParam> {
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 ) { if (tempValue.getName() == null || Tools.isEmpty(tempValue.getName().toString()) || Tools.isEmpty(tempValue.getName().toString())) { errorCount.add("导入数据必填项为空"); return ; } ... 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) { ... 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); } }; }
|
控制层的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @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(); 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快)