Java的web开发需要excel的导入导出工具,所以需要一定的工具类实现,如果是使用easypoi、Hutool导入导出excel,会非常的损耗内存,因此可以尝试使用easyexcel解决大数据量的数据的导入导出,且可以通过Java8的函数式编程解决该问题。
使用easyexcel,虽然不太会出现OOM的问题,但是如果是大数据量的情况下也会有一定量的内存溢出的风险,所以我打算从以下几个方面优化这个问题:
<dependency > <groupId > com.alibabagroupId > <artifactId > easyexcelartifactId > <version > 3.0.5version >dependency >
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
先实现一个类,用来指代导入的特定的对象
@Data @NoArgsConstructor @AllArgsConstructor @TableName ("stu_info" )@ApiModel ("学生信息" )//@ExcelIgnoreUnannotated 没有注解的字段都不转换 public class StuInfo { private static final long serialVersionUID = 1L ; /** * 姓名 */ // 设置字体,此处代表使用斜体 // @ContentFontStyle(italic = BooleanEnum.TRUE) // 设置列宽度的注解,注解中只有一个参数value,value的单位是字符长度,最大可以设置255个字符 @ColumnWidth (10 ) // @ExcelProperty 注解中有三个参数value,index,converter分别代表表名,列序号,数据转换方式 @ApiModelProperty ("姓名" ) @ExcelProperty (value = "姓名" ,order = 0 ) @ExportHeader (value = "姓名" ,index = 1 ) private String name; /** * 年龄 */ // @ExcelIgnore不将该字段转换成Excel @ExcelProperty (value = "年龄" ,order = 1 ) @ApiModelProperty ("年龄" ) @ExportHeader (value = "年龄" ,index = 2 ) private Integer age; /** * 身高 */ //自定义格式-位数 // @NumberFormat("#.##%") @ExcelProperty (value = "身高" ,order = 2 ) @ApiModelProperty ("身高" ) @ExportHeader (value = "身高" ,index = 4 ) private Double tall; /** * 自我介绍 */ @ExcelProperty (value = "自我介绍" ,order = 3 ) @ApiModelProperty ("自我介绍" ) @ExportHeader (value = "自我介绍" ,index = 3 ,ignore = true ) private String selfIntroduce; /** * 图片信息 */ @ExcelProperty (value = "图片信息" ,order = 4 ) @ApiModelProperty ("图片信息" ) @ExportHeader (value = "图片信息" ,ignore = true ) private Blob picture; /** * 性别 */ @ExcelProperty (value = "性别" ,order = 5 ) @ApiModelProperty ("性别" ) private Integer gender; /** * 入学时间 */ //自定义格式-时间格式 @DateTimeFormat ("yyyy-MM-dd HH:mm:ss:" ) @ExcelProperty (value = "入学时间" ,order = 6 ) @ApiModelProperty ("入学时间" ) private String intake; /** * 出生日期 */ @ExcelProperty (value = "出生日期" ,order = 7 ) @ApiModelProperty ("出生日期" ) private String birthday; }
重写ReadListener接口
@Slf 4jpublic class UploadDataListener <T > implements ReadListener <T > { /** * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 100 ; /** * 缓存的数据 */ private List cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); /** * Predicate用于过滤数据 */ private Predicate predicate; /** * 调用持久层批量保存 */ private Consumer> consumer; public UploadDataListener (Predicate predicate, Consumer> consumer) { this .predicate = predicate; this .consumer = consumer; } public UploadDataListener (Consumer> consumer) { this .consumer = consumer; } /** * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来 * * @param demoDAO */ /** * 这个每一条数据解析都会来调用 * * @param data one row value. Is is same as {@link AnalysisContext#readRowHolder()} * @param context */ @Override public void invoke (T data, AnalysisContext context) { if (predicate != null && !predicate.test(data)) { return ; } cachedDataList.add(data); // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM if (cachedDataList.size() >= BATCH_COUNT) { try { // 执行具体消费逻辑 consumer.accept(cachedDataList); } catch (Exception e) { log.error("Failed to upload data!data={}" , cachedDataList); throw new BizException("导入失败" ); } // 存储完成清理 list cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); } } /** * 所有数据解析完成了 都会来调用 * * @param
context */ @Override public void doAfterAllAnalysed (AnalysisContext context) { // 这里也要保存数据,确保最后遗留的数据也存储到数据库 if (CollUtil.isNotEmpty(cachedDataList)) { try { // 执行具体消费逻辑 consumer.accept(cachedDataList); log.info("所有数据解析完成!" ); } catch (Exception e) { log.error("Failed to upload data!data={}" , cachedDataList); // 抛出自定义的提示信息 if (e instanceof BizException) { throw e; } throw new BizException("导入失败" ); } } } }
Controller层的实现
@ApiOperation ("只需要一个readListener,解决全部的问题" )@PostMapping ("/update" )@ResponseBody public R aListener4AllExcel (MultipartFile file) throws IOException { try { EasyExcel.read(file.getInputStream(), StuInfo.class , new UploadDataListener <StuInfo >( list -> { // 校验数据 ValidationUtils.validate(list); // dao 保存··· //最好是手写一个,不要使用mybatis-plus的一条条新增的逻辑 service.saveBatch(list); log.info("从Excel导入数据一共 {} 行 " , list.size()); })) .sheet() .doRead(); } catch (IOException e) { log.error("导入失败" , e); throw new BizException("导入失败" ); } return R.success("SUCCESS" ); }
但是这种方式只能实现已存对象的功能实现,如果要新增一种数据的导入,那我们需要怎么做呢?
可以通过读取成Map,根据顺序导入到数据库中。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
Controller层的实现
@ApiOperation ("只需要一个readListener,解决全部的问题" )@PostMapping ("/listenMapDara" )@ResponseBody public R listenMapDara (@ApiParam(value = "表编码" , required = true ) @NotBlank (message = "表编码不能为空" ) @RequestParam ("tableCode" ) String tableCode, @ApiParam (value = "上传的文件" , required = true ) @NotNull (message = "上传文件不能为空" ) MultipartFile file) throws IOException { try { //根据tableCode获取这张表的字段,可以作为insert与剧中的信息 EasyExcel.read(file.getInputStream(), new NonClazzOrientedListener( list -> { // 校验数据 // ValidationUtils.validate(list); // dao 保存··· log.info("从Excel导入数据一共 {} 行 " , list.size()); })) .sheet() .doRead(); } catch (IOException e) { log.error("导入失败" , e); throw new BizException("导入失败" ); } return R.success("SUCCESS" ); }
重写ReadListener接口
@Slf 4jpublic class NonClazzOrientedListener implements ReadListener <Map <Integer , String >> { /** * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收 */ private static final int BATCH_COUNT = 100 ; private List> rowsList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT); private List rowList = new ArrayList<>(); /** * Predicate用于过滤数据 */ private Predicate> predicate; /** * 调用持久层批量保存 */ private Consumer consumer; public NonClazzOrientedListener (Predicate> predicate, Consumer consumer)
{ this .predicate = predicate; this .consumer = consumer; } public NonClazzOrientedListener (Consumer consumer)
{ this .consumer = consumer; } /** * 添加deviceName标识 */ private boolean flag = false ; @Override public void invoke (Map row, AnalysisContext analysisContext) { 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 public void doAfterAllAnalysed (AnalysisContext analysisContext) { consumer.accept(rowsList); if (CollUtil.isNotEmpty(rowsList)) { try { log.debug("执行最后的程序" ); log.info("rowsList is {}" , rowsList); } catch (Exception e) { log.error("Failed to upload data!data={}" , rowsList); // 抛出自定义的提示信息 if (e instanceof BizException) { throw e; } throw new BizException("导入失败" ); } finally { rowsList.clear(); } } }
这种方式可以通过把表中的字段顺序存储起来,通过配置数据和字段的位置实现数据的新增,那么如果出现了导出数据模板/手写excel的时候顺序和导入的时候顺序不一样怎么办?