Spring boot实现低代码量的Excel导入导出

Spring boot实现低代码量的Excel导入导出

[TOC]

2024年4月12日

Java的web开发需要excel的导入导出工具,所以需要一定的工具类实现,如果是使用easypoi、Hutool导入导出excel,会非常的损耗内存,因此可以尝试使用easyexcel解决大数据量的数据的导入导出,且可以通过Java8的函数式编程解决该问题。

使用easyexcel,虽然不太会出现OOM的问题,但是如果是大数据量的情况下也会有一定量的内存溢出的风险,所以我打算从以下几个方面优化这个问题:

使用Java8的函数式编程实现低代码量的数据导入

使用反射等特性实现单个接口导入任意excel

使用线程池实现大数据量的excel导入

通过泛型实现数据导出

maven导入

<!--EasyExcel相关依赖-->

<dependency>

<groupId>com.alibaba</groupId>

<artifactId>easyexcel</artifactId>

<version>3.0.5</version>

</dependency>

使用泛型实现对象的单个Sheet导入

先实现一个类,用来指代导入的特定的对象

@Data

@NoArgsConstructor

@AllArgsConstructor

@TableName("stu_info")

@ApiModel("学生信息")

//@ExcelIgnoreUnannotated 没有注解的字段都不转换

publicclassStuInfo{

privatestaticfinallongserialVersionUID=1L;

/**

* 姓名

*/

// 设置字体,此处代表使用斜体

//    @ContentFontStyle(italic = BooleanEnum.TRUE)

// 设置列宽度的注解,注解中只有一个参数value,value的单位是字符长度,最大可以设置255个字符

@ColumnWidth(10)

// @ExcelProperty 注解中有三个参数value,index,converter分别代表表名,列序号,数据转换方式

@ApiModelProperty("姓名")

@ExcelProperty(value="姓名",order=0)

@ExportHeader(value="姓名",index=1)

privateStringname;

/**

* 年龄

*/

//    @ExcelIgnore不将该字段转换成Excel

@ExcelProperty(value="年龄",order=1)

@ApiModelProperty("年龄")

@ExportHeader(value="年龄",index=2)

privateIntegerage;

/**

* 身高

*/

//自定义格式-位数

//    @NumberFormat("#.##%")

@ExcelProperty(value="身高",order=2)

@ApiModelProperty("身高")

@ExportHeader(value="身高",index=4)

privateDoubletall;

/**

* 自我介绍

*/

@ExcelProperty(value="自我介绍",order=3)

@ApiModelProperty("自我介绍")

@ExportHeader(value="自我介绍",index=3,ignore=true)

privateStringselfIntroduce;

/**

* 图片信息

*/

@ExcelProperty(value="图片信息",order=4)

@ApiModelProperty("图片信息")

@ExportHeader(value="图片信息",ignore=true)

privateBlobpicture;

/**

* 性别

*/

@ExcelProperty(value="性别",order=5)

@ApiModelProperty("性别")

privateIntegergender;

/**

* 入学时间

*/

//自定义格式-时间格式

@DateTimeFormat("yyyy-MM-dd HH:mm:ss:")

@ExcelProperty(value="入学时间",order=6)

@ApiModelProperty("入学时间")

privateStringintake;

/**

* 出生日期

*/

@ExcelProperty(value="出生日期",order=7)

@ApiModelProperty("出生日期")

privateStringbirthday;

}

重写ReadListener接口

@Slf4j

publicclassUploadDataListener<T>implementsReadListener<T>{

/**

* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收

*/

privatestaticfinalintBATCH_COUNT=100;

/**

* 缓存的数据

*/

privateList<T>cachedDataList=ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

/**

* Predicate用于过滤数据

*/

privatePredicate<T>predicate;

/**

* 调用持久层批量保存

*/

privateConsumer<Collection<T>>consumer;

publicUploadDataListener(Predicate<T>predicate,Consumer<Collection<T>>consumer) {

this.predicate=predicate;

this.consumer=consumer;

   }

publicUploadDataListener(Consumer<Collection<T>>consumer) {

this.consumer=consumer;

   }

/**

* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来

*

* @param demoDAO

*/

/**

* 这个每一条数据解析都会来调用

*

* @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}

* @param context

*/

@Override

publicvoidinvoke(Tdata,AnalysisContextcontext) {

if(predicate!=null&&!predicate.test(data)) {

return;

       }

cachedDataList.add(data);

// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM

if(cachedDataList.size()>=BATCH_COUNT) {

try{

// 执行具体消费逻辑

consumer.accept(cachedDataList);

}catch(Exceptione) {

log.error("Failed to upload data!data={}",cachedDataList);

thrownewBizException("导入失败");

           }

// 存储完成清理 list

cachedDataList=ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

       }

   }

/**

* 所有数据解析完成了 都会来调用

*

* @param context

*/

@Override

publicvoiddoAfterAllAnalysed(AnalysisContextcontext) {

// 这里也要保存数据,确保最后遗留的数据也存储到数据库

if(CollUtil.isNotEmpty(cachedDataList)) {

try{

// 执行具体消费逻辑

consumer.accept(cachedDataList);

log.info("所有数据解析完成!");

}catch(Exceptione) {

log.error("Failed to upload data!data={}",cachedDataList);

// 抛出自定义的提示信息

if(einstanceofBizException) {

throwe;

               }

thrownewBizException("导入失败");

           }

       }

   }

}

Controller层的实现

@ApiOperation("只需要一个readListener,解决全部的问题")

@PostMapping("/update")

@ResponseBody

publicR<String>aListener4AllExcel(MultipartFilefile)throwsIOException{

try{

EasyExcel.read(file.getInputStream(),

StuInfo.class,

newUploadDataListener<StuInfo>(

list->{

// 校验数据

//                                        ValidationUtils.validate(list);

// dao 保存···

//最好是手写一个,不要使用mybatis-plus的一条条新增的逻辑

service.saveBatch(list);

log.info("从Excel导入数据一共 {} 行 ",list.size());

                                   }))

.sheet()

.doRead();

}catch(IOExceptione) {

log.error("导入失败",e);

thrownewBizException("导入失败");

       }

returnR.success("SUCCESS");

   }

但是这种方式只能实现已存对象的功能实现,如果要新增一种数据的导入,那我们需要怎么做呢?

可以通过读取成Map,根据顺序导入到数据库中。

通过实现单个Sheet中任意一种数据的导入

Controller层的实现

@ApiOperation("只需要一个readListener,解决全部的问题")

@PostMapping("/listenMapDara")

@ResponseBody

publicR<String>listenMapDara(@ApiParam(value="表编码",required=true)

@NotBlank(message="表编码不能为空")

@RequestParam("tableCode")StringtableCode,

@ApiParam(value="上传的文件",required=true)

@NotNull(message="上传文件不能为空")MultipartFilefile)throwsIOException{

try{

//根据tableCode获取这张表的字段,可以作为insert与剧中的信息

EasyExcel.read(file.getInputStream(),

newNonClazzOrientedListener(

list->{

// 校验数据

//                                        ValidationUtils.validate(list);

// dao 保存···

log.info("从Excel导入数据一共 {} 行 ",list.size());

                                   }))

.sheet()

.doRead();

}catch(IOExceptione) {

log.error("导入失败",e);

thrownewBizException("导入失败");

       }

returnR.success("SUCCESS");

   }

重写ReadListener接口

@Slf4j

publicclassNonClazzOrientedListenerimplementsReadListener<Map<Integer,String>>{

/**

* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收

*/

privatestaticfinalintBATCH_COUNT=100;

privateList<List<Object>>rowsList=ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

privateList<Object>rowList=newArrayList<>();

/**

* Predicate用于过滤数据

*/

privatePredicate<Map<Integer,String>>predicate;

/**

* 调用持久层批量保存

*/

privateConsumer<List>consumer;

publicNonClazzOrientedListener(Predicate<Map<Integer,String>>predicate,Consumer<List>consumer) {

this.predicate=predicate;

this.consumer=consumer;

   }

publicNonClazzOrientedListener(Consumer<List>consumer) {

this.consumer=consumer;

   }

/**

* 添加deviceName标识

*/

privatebooleanflag=false;

@Override

publicvoidinvoke(Map<Integer,String>row,AnalysisContextanalysisContext) {

consumer.accept(rowsList);

rowList.clear();

row.forEach((k,v)->{

log.debug("key is {},value is {}",k,v);

rowList.add(v==null?"":v);

       });

rowsList.add(rowList);

if(rowsList.size()>BATCH_COUNT) {

log.debug("执行存储程序");

log.info("rowsList is {}",rowsList);

rowsList.clear();

       }

   }

@Override

publicvoiddoAfterAllAnalysed(AnalysisContextanalysisContext) {

consumer.accept(rowsList);

if(CollUtil.isNotEmpty(rowsList)) {

try{

log.debug("执行最后的程序");

log.info("rowsList is {}",rowsList);

}catch(Exceptione) {

log.error("Failed to upload data!data={}",rowsList);

// 抛出自定义的提示信息

if(einstanceofBizException) {

throwe;

               }

thrownewBizException("导入失败");

}finally{

rowsList.clear();

           }

       }

   }

这种方式可以通过把表中的字段顺序存储起来,通过配置数据和字段的位置实现数据的新增,那么如果出现了导出数据模板/手写excel的时候顺序和导入的时候顺序不一样怎么办?

可以通过读取header进行实现,通过表头读取到的字段,和数据库中表的字段进行比对,只取其中存在的数据进行排序添加

/**

* 这里会一行行的返回头

*

* @param headMap

* @param context

*/

@Override

publicvoidinvokeHead(Map<Integer,ReadCellData<?>>headMap,AnalysisContextcontext) {

//该方法必然会在读取数据之前进行

Map<Integer,String>columMap=ConverterUtils.convertToStringMap(headMap,context);

//通过数据交互拿到这个表的表头

//        Map<String,String> columnList=dao.xxxx();

Map<String,String>columnList=newHashMap();

columMap.forEach((key,value)->{

if(columnList.containsKey(value)) {

filterList.add(key);

           }

       });

//过滤到了只存在表里面的数据,顺序就不用担心了,可以直接把filterList的数据用于排序,可以根据mybatis做一个动态sql进行应用

log.info("解析到一条头数据:{}",JSON.toJSONString(columMap));

// 如果想转成成 Map<Integer,String>

// 方案1: 不要implements ReadListener 而是 extends AnalysisEventListener

// 方案2: 调用 ConverterUtils.convertToStringMap(headMap, context) 自动会转换

   }

那么这些问题都解决了,如果出现大数据量的情况,如果要极大的使用到cpu,该怎么做呢?

可以尝试使用线程池进行实现

使用线程池进行多线程导入大量数据

Java中线程池的开发与使用与原理我可以单独写一篇文章进行讲解,但是在这边为了进行好的开发我先给出一套固定一点的方法。

由于ReadListener不能被注册到IOC容器里面,所以需要在外面开启

详情可见Spring Boot通过EasyExcel异步多线程实现大数据量Excel导入,百万数据30秒

通过泛型实现对象类型的导出

public<T>voidcommonExport(StringfileName,List<T>data,Class<T>clazz,HttpServletResponseresponse)throwsIOException{

if(CollectionUtil.isEmpty(data)) {

data=newArrayList<>();

       }

//设置标题

fileName=URLEncoder.encode(fileName,"UTF-8");

response.setContentType("application/vnd.ms-excel");

response.setCharacterEncoding("utf-8");

response.setHeader("Content-disposition","attachment;filename="+fileName+".xlsx");

EasyExcel.write(response.getOutputStream()).head(clazz).sheet("sheet1").doWrite(data);

   }

直接使用该方法可以作为公共的数据的导出接口

如果想要动态的下载任意一组数据怎么办呢?可以使用这个方法

publicvoidexportFreely(StringfileName,List<List<Object>>data,List<List<String>>head,HttpServletResponseresponse)throwsIOException{

if(CollectionUtil.isEmpty(data)) {

data=newArrayList<>();

       }

//设置标题

fileName=URLEncoder.encode(fileName,"UTF-8");

response.setContentType("application/vnd.ms-excel");

response.setCharacterEncoding("utf-8");

response.setHeader("Content-disposition","attachment;filename="+fileName+".xlsx");

EasyExcel.write(response.getOutputStream()).head(head).sheet("sheet1").doWrite(data);

   }

什么?不仅想一个接口展示全部的数据与信息,还要增加筛选条件?这个后期我可以单独写一篇文章解决这个问题。

今天的分享就到这里了。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容