专栏名称: Java基基
一个苦练基本功的 Java 公众号,所以取名 Java 基基
目录
相关文章推荐
丁香园  ·  这类患者降压 55~85 ... ·  昨天  
肿瘤资讯  ·  丽证一线 | 直击绝经后HR+ / ... ·  4 天前  
蒲公英Ouryao  ·  NMPA:三个中药品种被保护 ·  3 天前  
51好读  ›  专栏  ›  Java基基

SpringBoot一个接口实现任意表的 Excel 导入导出

Java基基  · 公众号  ·  · 2025-02-13 11:55

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、 商城 、支付、工作流、大屏报表、微信公众号、 ERP CRM AI 大模型 等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本

来源:juejin.cn/post/
7356508172419481663


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

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

  • 使用Java8的函数式编程实现低代码量的数据导入
  • 使用反射等特性实现单个接口导入任意excel
  • 使用线程池实现大数据量的excel导入
  • 通过泛型实现数据导出

maven导入


<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/

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

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

@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接口

@Slf4j
public class UploadDataListener<Timplements 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/

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

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接口

@Slf4j
public class NonClazzOrientedListener implements ReadListener<Map<IntegerString>> {

    /**
     * 每隔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的时候顺序和导入的时候顺序不一样怎么办?







请到「今天看啥」查看全文