SpringBoot整合EasyExcel实现数据导入导出
一、前言
在公司业务系统开发过程中,操作 Excel 实现数据的导入导出是个非常常见的需求。
最近公司的项目采用EasyPoi来实现的,但是在数据量大的情况下,EasyPoi 会占用内存大,性能不够好,严重的时候,还会出现内存异常的现象。
项目二期改造就换为性能更好的 Excel 导入导出工具:EasyExcel。
EasyExcel 的处理数据性能非常高,读取 75M (46W行25列) 的Excel,仅需使用 64M 内存,耗时 20s,极速模式还可以更快!
二、实践
在 SpringBoot 项目中集成 EasyExcel 依赖,pom.xml加入:
<!--EasyExcel相关依赖--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.0.5</version></dependency>
EasyExcel 的导出导入支持两种方式进行处理:
- 第一种是通过实体类注解方式来生成文件和反解析文件数据映射成对象
- 第二种是通过动态参数化生成文件和反解析文件数据
2.1 实体类注解方式
创建UserEntity用户实体类,然后添加对应的注解字段即可:
publicclassUserWriteEntity{@ExcelProperty(value ="姓名")privateString name;@ExcelProperty(value ="年龄")privateint age;@DateTimeFormat("yyyy-MM-dd HH:mm:ss")@ExcelProperty(value ="操作时间")privateDate time;//set、get...}
使用 EasyExcel 提供的EasyExcel工具类,即可实现文件的导出:
publicstaticvoidmain(String[] args)throwsFileNotFoundException{List<UserWriteEntity> dataList =newArrayList<>();for(int i =0; i <10; i++){UserWriteEntity userEntity =newUserWriteEntity();
userEntity.setName("张三"+ i);
userEntity.setAge(20+ i);
userEntity.setTime(newDate(System.currentTimeMillis()+ i));
dataList.add(userEntity);}//定义文件输出位置FileOutputStream outputStream =newFileOutputStream(newFile("/Users/panzhi/Documents/easyexcel-export-user1.xlsx"));EasyExcel.write(outputStream,UserWriteEntity.class).sheet("用户信息").doWrite(dataList);}
运行程序,打开文件内容结果:
使用 EasyExcel 提供的EasyExcel工具类实现简单固定表头的 Excel 文件导入:
创建读取实体类:
/**
* 读取实体类
*/publicclassUserReadEntity{@ExcelProperty(value ="姓名")privateString name;/**
* 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
*/@ExcelProperty(index =1)privateint age;@DateTimeFormat("yyyy-MM-dd HH:mm:ss")@ExcelProperty(value ="操作时间")privateDate time;//set、get...}
读取文件数据,并封装到对象里面:
publicstaticvoidmain(String[] args)throwsFileNotFoundException{//同步读取文件内容FileInputStream inputStream =newFileInputStream(newFile("/Users/panzhi/Documents/easyexcel-user1.xls"));List<UserReadEntity> list =EasyExcel.read(inputStream).head(UserReadEntity.class).sheet().doReadSync();System.out.println(JSONArray.toJSONString(list));}
运行程序,输出结果如下:
[{"age":20,"name":"张三0","time":1616920360000},{"age":21,"name":"张三1","time":1616920360000},{"age":22,"name":"张三2","time":1616920360000},{"age":23,"name":"张三3","time":1616920360000},{"age":24,"name":"张三4","time":1616920360000},{"age":25,"name":"张三5","time":1616920360000},{"age":26,"name":"张三6","time":1616920360000},{"age":27,"name":"张三7","time":1616920360000},{"age":28,"name":"张三8","time":1616920360000},{"age":29,"name":"张三9","time":1616920360000}]
2.2 动态参数化导出导入
在实际使用开发中,不可能每个 excel 导入导出需求,都创建实体类,很多业务需求需要根据不同的字段来动态导入导出,没办法基于实体类注解的方式来读取文件或者写入文件。
因此,基于EasyExcel提供的动态参数化生成文件和动态监听器读取文件方法,可以单独封装一套动态导出导出工具类,避免大量重复工作。
动态导出工具类:
publicclassDynamicEasyExcelExportUtils{privatestaticfinalLogger log =LoggerFactory.getLogger(DynamicEasyExcelExportUtils.class);privatestaticfinalStringDEFAULT_SHEET_NAME="sheet1";/**
* 动态生成导出模版(单表头)
* @param headColumns 列名称
* @return excel文件流
*/publicstaticbyte[]exportTemplateExcelFile(List<String> headColumns){List<List<String>> excelHead =Lists.newArrayList();
headColumns.forEach(columnName ->{ excelHead.add(Lists.newArrayList(columnName));});byte[] stream =createExcelFile(excelHead,newArrayList<>());return stream;}/**
* 动态生成模版(复杂表头)
* @param excelHead 列名称
* @return
*/publicstaticbyte[]exportTemplateExcelFileCustomHead(List<List<String>> excelHead){byte[] stream =createExcelFile(excelHead,newArrayList<>());return stream;}/**
* 动态导出文件(通过map方式计算)
* @param headColumnMap 有序列头部
* @param dataList 数据体
* @return
*/publicstaticbyte[]exportExcelFile(LinkedHashMap<String,String> headColumnMap,List<Map<String,Object>> dataList){//获取列名称List<List<String>> excelHead =newArrayList<>();if(MapUtils.isNotEmpty(headColumnMap)){//key为匹配符,value为列名,如果多级列名用逗号隔开
headColumnMap.entrySet().forEach(entry ->{
excelHead.add(Lists.newArrayList(entry.getValue().split(",")));});}List<List<Object>> excelRows =newArrayList<>();if(MapUtils.isNotEmpty(headColumnMap)&&CollectionUtils.isNotEmpty(dataList)){for(Map<String,Object> dataMap : dataList){List<Object> rows =newArrayList<>();
headColumnMap.entrySet().forEach(headColumnEntry ->{if(dataMap.containsKey(headColumnEntry.getKey())){Object data = dataMap.get(headColumnEntry.getKey());
rows.add(data);}});
excelRows.add(rows);}}byte[] stream =createExcelFile(excelHead, excelRows);return stream;}/**
* 生成文件(自定义头部排列)
* @param rowHeads
* @param excelRows
* @return
*/publicstaticbyte[]customerExportExcelFile(List<List<String>> rowHeads,List<List<Object>> excelRows){//将行头部转成easyexcel能识别的部分List<List<String>> excelHead =transferHead(rowHeads);returncreateExcelFile(excelHead, excelRows);}/**
* 生成文件
* @param excelHead
* @param excelRows
* @return
*/privatestaticbyte[]createExcelFile(List<List<String>> excelHead,List<List<Object>> excelRows){try{if(CollectionUtils.isNotEmpty(excelHead)){ByteArrayOutputStream outputStream =newByteArrayOutputStream();EasyExcel.write(outputStream).registerWriteHandler(newLongestMatchColumnWidthStyleStrategy()).head(excelHead).sheet(DEFAULT_SHEET_NAME).doWrite(excelRows);return outputStream.toByteArray();}}catch(Exception e){
log.error("动态生成excel文件失败,headColumns:"+JSONArray.toJSONString(excelHead)+",excelRows:"+JSONArray.toJSONString(excelRows), e);}returnnull;}/**
* 将行头部转成easyexcel能识别的部分
* @param rowHeads
* @return
*/publicstaticList<List<String>>transferHead(List<List<String>> rowHeads){//将头部列进行反转List<List<String>> realHead =newArrayList<>();if(CollectionUtils.isNotEmpty(rowHeads)){Map<Integer,List<String>> cellMap =newLinkedHashMap<>();//遍历行for(List<String> cells : rowHeads){//遍历列for(int i =0; i < cells.size(); i++){if(cellMap.containsKey(i)){
cellMap.get(i).add(cells.get(i));}else{
cellMap.put(i,Lists.newArrayList(cells.get(i)));}}}//将列一行一行加入realHead
cellMap.entrySet().forEach(item -> realHead.add(item.getValue()));}return realHead;}/**
* 导出文件测试
* @param args
* @throws IOException
*/publicstaticvoidmain(String[] args)throwsIOException{//导出包含数据内容的文件(方式一)LinkedHashMap<String,String> headColumnMap =Maps.newLinkedHashMap();
headColumnMap.put("className","班级");
headColumnMap.put("name","学生信息,姓名");
headColumnMap.put("sex","学生信息,性别");List<Map<String,Object>> dataList =newArrayList<>();for(int i =0; i <5; i++){Map<String,Object> dataMap =Maps.newHashMap();
dataMap.put("className","一年级");
dataMap.put("name","张三"+ i);
dataMap.put("sex","男");
dataList.add(dataMap);}byte[] stream1 =exportExcelFile(headColumnMap, dataList);FileOutputStream outputStream1 =newFileOutputStream(newFile("/Users/panzhi/Documents/easyexcel-export-user5.xlsx"));
outputStream1.write(stream1);
outputStream1.close();//导出包含数据内容的文件(方式二)//头部,第一层List<String> head1 =newArrayList<>();
head1.add("第一行头部列1");
head1.add("第一行头部列1");
head1.add("第一行头部列1");
head1.add("第一行头部列1");//头部,第二层List<String> head2 =newArrayList<>();
head2.add("第二行头部列1");
head2.add("第二行头部列1");
head2.add("第二行头部列2");
head2.add("第二行头部列2");//头部,第三层List<String> head3 =newArrayList<>();
head3.add("第三行头部列1");
head3.add("第三行头部列2");
head3.add("第三行头部列3");
head3.add("第三行头部列4");//封装头部List<List<String>> allHead =newArrayList<>();
allHead.add(head1);
allHead.add(head2);
allHead.add(head3);//封装数据体//第一行数据List<Object> data1 =Lists.newArrayList(1,1,1,1);//第二行数据List<Object> data2 =Lists.newArrayList(2,2,2,2);List<List<Object>> allData =Lists.newArrayList(data1, data2);byte[] stream2 =customerExportExcelFile(allHead, allData);FileOutputStream outputStream2 =newFileOutputStream(newFile("/Users/panzhi/Documents/easyexcel-export-user6.xlsx"));
outputStream2.write(stream2);
outputStream2.close();}}
动态导入工具类:
/**
* 创建一个文件读取监听器
*/publicclassDynamicEasyExcelListenerextendsAnalysisEventListener<Map<Integer,String>>{privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(UserDataListener.class);/**
* 表头数据(存储所有的表头数据)
*/privateList<Map<Integer,String>> headList =newArrayList<>();/**
* 数据体
*/privateList<Map<Integer,String>> dataList =newArrayList<>();/**
* 这里会一行行的返回头
*
* @param headMap
* @param context
*/@OverridepublicvoidinvokeHeadMap(Map<Integer,String> headMap,AnalysisContext context){LOGGER.info("解析到一条头数据:{}",JSON.toJSONString(headMap));//存储全部表头数据
headList.add(headMap);}/**
* 这个每一条数据解析都会来调用
*
* @param data
* one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/@Overridepublicvoidinvoke(Map<Integer,String> data,AnalysisContext context){LOGGER.info("解析到一条数据:{}",JSON.toJSONString(data));
dataList.add(data);}/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/@OverridepublicvoiddoAfterAllAnalysed(AnalysisContext context){// 这里也要保存数据,确保最后遗留的数据也存储到数据库LOGGER.info("所有数据解析完成!");}publicList<Map<Integer,String>>getHeadList(){return headList;}publicList<Map<Integer,String>>getDataList(){return dataList;}}
/**
* 编写导入工具类
*/publicclassDynamicEasyExcelImportUtils{/**
* 动态获取全部列和数据体,默认从第一行开始解析数据
* @param stream
* @return
*/publicstaticList<Map<String,String>>parseExcelToView(byte[] stream){returnparseExcelToView(stream,1);}/**
* 动态获取全部列和数据体
* @param stream excel文件流
* @param parseRowNumber 指定读取行
* @return
*/publicstaticList<Map<String,String>>parseExcelToView(byte[] stream,Integer parseRowNumber){DynamicEasyExcelListener readListener =newDynamicEasyExcelListener();EasyExcelFactory.read(newByteArrayInputStream(stream)).registerReadListener(readListener).headRowNumber(parseRowNumber).sheet(0).doRead();List<Map<Integer,String>> headList = readListener.getHeadList();if(CollectionUtils.isEmpty(headList)){thrownewRuntimeException("Excel未包含表头");}List<Map<Integer,String>> dataList = readListener.getDataList();if(CollectionUtils.isEmpty(dataList)){thrownewRuntimeException("Excel未包含数据");}//获取头部,取最后一次解析的列头数据Map<Integer,String> excelHeadIdxNameMap = headList.get(headList.size()-1);//封装数据体List<Map<String,String>> excelDataList =Lists.newArrayList();for(Map<Integer,String> dataRow : dataList){Map<String,String> rowData =newLinkedHashMap<>();
excelHeadIdxNameMap.entrySet().forEach(columnHead ->{
rowData.put(columnHead.getValue(), dataRow.get(columnHead.getKey()));});
excelDataList.add(rowData);}return excelDataList;}/**
* 文件导入测试
* @param args
* @throws IOException
*/publicstaticvoidmain(String[] args)throwsIOException{FileInputStream inputStream =newFileInputStream(newFile("/Users/panzhi/Documents/easyexcel-export-user5.xlsx"));byte[] stream =IoUtils.toByteArray(inputStream);List<Map<String,String>> dataList =parseExcelToView(stream,2);System.out.println(JSONArray.toJSONString(dataList));
inputStream.close();}}
为方便后续的操作流程,在解析数据的时候,会将列名作为key。
EasyExcel 的功能还不只上面介绍的那些内容,还有基于模版进行 excel的填充,web 端 restful 的导出导出,使用方法大致都差不多。
版权归原作者 容若只如初见 所有, 如有侵权,请联系我们删除。