EasyExcel简单使用

2023-11-15

EasyExcel简单使用

EasyExcel是阿里开源的一个Excel处理工具。官网这么介绍:

EasyExcel是一个基于Java的、快速、简洁、解决大文件内存溢出的Excel处理工具。他能让你在不用考虑性能、内存的等因素的情况下,快速完成Excel的读、写等功能。

官网地址: https://easyexcel.opensource.alibaba.com/

我的项目也饱受excel处理的性能和内存等问题折磨,中间尝试过限制导出并发数、单独开启一个服务导出、使用队列排队等方式,最终的处理方式是单独开启服务处理+队列排队+并发限制+EasyExcel。下面介绍我对EasyExcel的一些心得。

1 读取Excel(导入)

引入maven依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.2.1</version>
</dependency>

1.1 简单读

这里提供4种读取方式,前2种不需要额外写监听器

@Test
public void simpleRead() {
    // 写法1:JDK8+ ,不用额外写一个DemoDataListener
    // since: 3.0.0-beta1
    String fileName = "d:/demo.xlsx";
    // 这里默认每次会读取100条数据 然后返回过来 直接调用使用数据就行
    // 具体需要返回多少行可以在`PageReadListener`的构造函数设置
    EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> {
        for (DemoData demoData : dataList) {
            log.info("读取到一条数据{}", JSON.toJSONString(demoData));
        }
    })).sheet().doRead();

    // 写法2:
    // 匿名内部类 不用额外写一个DemoDataListener
    fileName = "d:/demo.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
    EasyExcel.read(fileName, DemoData.class, new ReadListener<DemoData>() {
        /**
         * 单次缓存的数据量
         */
        public static final int BATCH_COUNT = 100;
        /**
         *临时存储
         */
        private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);

        @Override
        public void invoke(DemoData data, AnalysisContext context) {
            cachedDataList.add(data);
            if (cachedDataList.size() >= BATCH_COUNT) {
                saveData();
                // 存储完成清理 list
                cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext context) {
            saveData();
        }

        /**
         * 加上存储数据库
         */
        private void saveData() {
            log.info("{}条数据,开始存储数据库!", cachedDataList.size());
            log.info("存储数据库成功!");
        }
    }).sheet().doRead();

    // 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
    // 写法3:
    fileName = "d:/demo.xlsx";
    // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
    EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();

    // 写法4
    fileName = "d:/demo.xlsx";
    // 一个文件一个reader
    try (ExcelReader excelReader = EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).build()) {
        // 构建一个sheet 这里可以指定名字或者no
        ReadSheet readSheet = EasyExcel.readSheet(0).build();
        // 读取一个sheet
        excelReader.read(readSheet);
    }
}

DemoDataListener

public class DemoDataListener implements ReadListener<DemoData> {
    Logger log = LoggerFactory.getLogger(this.getClass());
    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;
    /**
     * 缓存的数据
     */
    private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
    /**
     * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private DemoDAO demoDAO;

    public DemoDataListener() {
        // 这里是demo,所以随便new一个。实际使用如果到了spring,请使用下面的有参构造函数
        demoDAO = new DemoDAO();
    }

    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param demoDAO
     */
    public DemoDataListener(DemoDAO demoDAO) {
        this.demoDAO = demoDAO;
    }

    /**
     * 这个每一条数据解析都会来调用
     *
     * @param data    one row value. Is is same as {@link AnalysisContext#readRowHolder()}
     * @param context
     */
    @Override
    public void invoke(DemoData data, AnalysisContext context) {
        log.info("解析到一条数据:{}", JSON.toJSONString(data));
        cachedDataList.add(data);
        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedDataList.size() >= BATCH_COUNT) {
            saveData();
            // 存储完成清理 list
            cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
        }
    }

    /**
     * 所有数据解析完成了 都会来调用
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveData();
        log.info("所有数据解析完成!");
    }

    /**
     * 加上存储数据库
     */
    private void saveData() {
        log.info("{}条数据,开始存储数据库!", cachedDataList.size());
        demoDAO.save(cachedDataList);
        log.info("存储数据库成功!");
    }
}

DemoData

public class DemoData {
    private String string;
    private Date date;
    private Double doubleData;

    public String getString() {
        return string;
    }

    public void setString(String string) {
        this.string = string;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public Double getDoubleData() {
        return doubleData;
    }

    public void setDoubleData(Double doubleData) {
        this.doubleData = doubleData;
    }
}

DemoDAO(简单示例)

public class DemoDAO {
    public void save(List<DemoData> list) {
        // 如果是mybatis,尽量别直接调用多次insert,自己写一个mapper里面新增一个方法批量insert,所有数据一次性插入
    }
}

1.2 指定下标或列名、类型转换

public class DemoData {
    /**
     * 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
     * 百分比的数字
     */
    @ExcelProperty(index = 2)
    @NumberFormat("#.##%")
    private Double doubleData;
    /**
     * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
     * 使用了日期格式化,要用string类型
     */
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    private String date;

    public String getString() {
        return string;
    }

    public void setString(String string) {
        this.string = string;
    }

    public String getDate() {
        return date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public Double getDoubleData() {
        return doubleData;
    }

    public void setDoubleData(Double doubleData) {
        this.doubleData = doubleData;
    }
}

1.3 多行头

// 这里 需要指定读用哪个class去读,然后读取第一个sheet 
// 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据DemoData 来解析,他没有指定头,也就是默认1行
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().headRowNumber(1).doRead();

1.4 多sheet


// 读取全部sheet
// 这里需要注意 DemoDataListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个DemoDataListener里面写
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).doReadAll();
// 读取部分sheet
try (ExcelReader excelReader = EasyExcel.read(fileName).build()) {
    // 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
    ReadSheet readSheet1 = EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
    ReadSheet readSheet2 = EasyExcel.readSheet(1).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
    // 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
    excelReader.read(readSheet1, readSheet2);
}

2.写入Excel(导出)

实体类DemoData与读取时基本类似

2.1 简单写

public void simpleWrite() {
    // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入

    // 写法1 JDK8+
    // since: 3.0.0-beta1
    String fileName = "d:/demo" + System.currentTimeMillis() + ".xlsx";
    // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    // 如果这里想使用03 则 传入excelType参数即可
    EasyExcel.write(fileName, DemoData.class)
            .sheet("模板")
            .doWrite(() -> {
                // 分页查询数据
                return data();
            });

    // 写法2
    fileName = "d:/demo" + System.currentTimeMillis() + ".xlsx";
    // 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
    // 如果这里想使用03 则 传入excelType参数即可
    EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());

    // 写法3
    fileName = "d:/demo" + System.currentTimeMillis() + ".xlsx";
    // 这里 需要指定写用哪个class去写
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
        WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
        excelWriter.write(data(), writeSheet);
    }
}

private List<DemoData> data() {
    List<DemoData> list = ListUtils.newArrayList();
    for (int i = 0; i < 10; i++) {
        DemoData data = new DemoData();
        data.setString("字符串" + i);
        data.setDate(new Date());
        data.setDoubleData(0.56);
        list.add(data);
    }
    return list;
}

2.2 重复多次写入

重复多次写入,当写入的量较大时,建议使用这种方式。常规数据单次写入量不超过5000,如果单条数据较大,5000的量还可以减小

public void repeatedWrite() {
    // 方法1: 如果写到同一个sheet
    String fileName = "d:/demo" + System.currentTimeMillis() + ".xlsx";
    // 这里 需要指定写用哪个class去写
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
        // 这里注意 如果同一个sheet只要创建一次
        WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
        for (int i = 0; i < 5; i++) {
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<DemoData> data = data();
            excelWriter.write(data, writeSheet);
        }
    }

    // 方法2: 如果写到不同的sheet 同一个对象
    fileName = "d:/demo" + System.currentTimeMillis() + ".xlsx";
    // 这里 指定文件
    try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
        for (int i = 0; i < 5; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
            WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<DemoData> data = data();
            excelWriter.write(data, writeSheet);
        }
    }

    // 方法3 如果写到不同的sheet 不同的对象
    fileName = "d:/demo" + System.currentTimeMillis() + ".xlsx";
    // 这里 指定文件
    try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
        // 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
        for (int i = 0; i < 5; i++) {
            // 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
            // 实际上可以一直变
            WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(DemoData.class).build();
            // 分页去数据库查询数据 这里可以去数据库查询每一页的数据
            List<DemoData> data = data();
            excelWriter.write(data, writeSheet);
        }
    }

}

2.3 其他特性

EasyExcel还支持复杂表头、格式转换、图片、超链接、备注、公式、根据模板写入、单元格合并、等等特性。具体可参看官方文档

3. 实用场景: excel导入后,将未执行成功的数据保存到excel并反馈给用户

下一篇来写

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

EasyExcel简单使用 的相关文章

随机推荐

  • np.mean()和np.std()函数

    一 np mean 函数定义 numpy mean a axis dtype out keepdims mean 函数功能 求取均值 经常操作的参数为axis 以m n矩阵举例 axis 不设置值 对 m n 个数求均值 返回一个实数 ax
  • kibana启动问题:Kibana server is not ready yet

    第一点 KB ES版本不一致 网上大部分都是这么说的 解决方法 把KB和ES版本调整为统一版本 第二点 kibana yml中配置有问题 通过查看日志 发现了Error No Living connections的问题 解决方法 将配置文件
  • Vue3通透教程【八】获取DOM、操作组件

    文章目录 写在前面 Vue2 ref 的使用 Vue3获取DOM Vue3操作组件 写在最后 写在前面 专栏介绍 凉哥作为 Vue 的忠实 粉丝输出过大量的 Vue 文章 应粉丝要求开始更新 Vue3 的相关技术文章 Vue 框架目前的地位
  • 【Linux】计算机操作系统和软硬件体系结构

    目录 1 冯诺依曼体系结构 1 1 中央处理器 CPU 2 操作系统 OS 2 1 操作系统的概念 2 2 操作系统的作用 2 3 操作系统如何进行管理 2 3 1 操作系统通过分级管理的方式 实现对整体的管理 2 3 2 管理的本质是对数
  • 如何使用chrome 浏览器自带截屏?

    1 ctrl shift c 2 ctrl shift p 3 输入 capture 4 选择capture full size screenshot 实现截取整个网页
  • enscape各种材质参数_它来了!Enscape专属素材库!

    ENSCAPE素材库 近两年风靡整个设计的渲染器 一款实时渲染软件Enscape 凭借入门低 成效快 效果逼真 从渲染界脱颖而出 用其他渲染器1 的时间渲出VRAY等老大哥级别90 的效果 作为建筑师的你会怎么选择呢 简单的分析了下渲染界最
  • MySQL connector/C++ 连接mysql效率低下解决

    这个问题 说解决也不算是被解决了 只能是让数据库插入的时候不会有像直接插入一样有那么多的问题了 我的解决方法是 开启mysql的事务 开始我也不知道是不是我的mysql配置优化的问题 WAMP统一安装 无限默认下一步的 在用PHP测试的时候
  • int与byte[]之间进行转换

    如何将int与byte 之间转换 int类型在内存中占4个字节 采用补码方式存储 而一个byte占一个字节 下面有两种方法进行转换 package cn fh vertxboot utils description int与Byte数组转换
  • 苹果app上架流程之傻瓜式教程剖析

    iOS开发者开发好一款APP之后 进行内测后没问题 下一步就是要上架AppStore了 一些开发者不知道该如何上架AppStore 下面 我们来说说iOS上架流程 以及如何快速上架AppStore 工具 1 iOS开发者账号 2 App U
  • Linux内存泄露案例分析和内存管理分享

    一 问题 近期我们运维同事接到线上LB 负载均衡 服务内存报警 运维同事反馈说LB集群有部分机器的内存使用率超过80 有的甚至超过90 而且内存使用率还再不停的增长 接到内存报警的消息 让整个团队都比较紧张 我们团队负责的LB服务是零售 物
  • Xamarin.Forms(移动应用)轮盘抽签软件(Android)

    该文章由我前面的文章https blog csdn net dabo 520 article details 129760956 spm 1001 2014 3001 5501改编而来 它是程序的核心 具体详细可自行前往观看 1 软件开发准
  • 基于Python的顺序表实现一元多项式相加

    具体代码 from operator import itemgetter class PolyList def init self self data def Add self e self data append e def Create
  • JavaWeb案例:实现注册和登录功能

    业务需求分析 在实际开发中 通常会有专门的人去跟客户进行沟通从而了解客户需要什么样的系统 之后由专业的美工将要做的系统以图片的形式表现出来 客户确认后作出一些静态的html demo页面 然后由软件开发人员创建相关数据库 编写代码将该静态页
  • python多线程_Python多线程爬虫,效率真的高

    有些时候 比如下载图片 因为下载图片是一个耗时的操作 如果采用之前那种同步的方式下载 那效率肯会特别慢 这时候我们就可以考虑使用多线程的方式来下载图片 多线程介绍 多线程是为了同步完成多项任务 通过提高资源使用效率来提高系统的效率 线程是在
  • PAT 乙级 1033 旧键盘打字 python

    题目 思路 因为坏键盘的输入是大写字母 遍历输入的字符 将输入字母的字符转换为大写 与坏键盘对比 如果 坏掉 当字母字符不在坏键盘之列 则是小写时 字符才能输出 代码 import sys bad key sys stdin readlin
  • Python语言实现批量视频分帧,保存视频帧

    本篇博客介绍利用python脚本实现视频分帧 并将每一帧保存到本地 主要基于opencv包来实现 在运行代码前确保opencv包已正确安装 下面是主要代码 import os import cv2 videos src path home
  • 大模型落地金融业,想象力在哪?

    金融大模型的难点在于 能否在产业中扎得更深 其颠覆性也更建立在 纵深到产业中去 赋能金融行业的长尾场景发展 以及重拾 金融信任 作者 思杭 编辑 皮爷 出品 产业家 从经济角度讲 整个金融业的数字化进程并非匀速 从技术角度讲 催化剂的出现会
  • TypeError: parse() got an unexpected keyword argument 'transport_encoding'

    import cv2时发现没有这个包 然后就安装一下 结果发现安装时出错了 错误如下 注 我是在pycharm里面配的anaconda 然后利用anaconda安装cv2 发现pip版本太低 我的是9 0 1 新的已经是10 0 1 于是就
  • STM32单片机学习记录3——GPIO(上)输出模式之点亮LED灯

    1 硬件准备 我使用的是市面上常见的黑色开发板 烧入器使用的是正点原子的无线烧入器 普通的烧入器也行 这个无所谓 开发板的原理图我放在下面链接里 我们需要知道相应的LED引脚 2 预期功能 通过函数实现LED灯的闪烁 这里直接采用模块化编程
  • EasyExcel简单使用

    EasyExcel简单使用 EasyExcel是阿里开源的一个Excel处理工具 官网这么介绍 EasyExcel是一个基于Java的 快速 简洁 解决大文件内存溢出的Excel处理工具 他能让你在不用考虑性能 内存的等因素的情况下 快速完