百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

EasyExcel 优雅实现 Excel 导入导出

ztj100 2024-11-05 13:28 20 浏览 0 评论

一、简介

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

二、特点

快速

快速的读取excel中的数据。

简洁

映射excel和实体类,让代码变的更加简洁。

节约内存

在读写大文件的时候使用磁盘做缓存,更加的节约内存。

16M内存23秒读取75M(46W行25列)的Excel(3.2.1+版本)

当然还有极速模式能更快,但是内存占用会在100M多一点。

三、对比

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。

easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便。

四、注解

@ExcelProperty

用于匹配excel和实体类的匹配,参数如下:

名称

默认值

描述

value

用于匹配excel中的头,必须全匹配,如果有多行头,会匹配最后一行头

order

Integer.MAX_VALUE

优先级高于value,会根据order的顺序来匹配实体和excel中数据的顺序

index

-1

优先级高于value和order,会根据index直接指定到excel中具体的哪一列

converter

自动选择

指定当前字段用什么转换器,默认会自动选择。读的情况下只要实现com.alibaba.excel.converters.Converter#convertToJavaData(com.alibaba.excel.converters.ReadConverterContext<?>) 方法即可


@ExcelIgnore

默认所有字段都会和excel去匹配,加了这个注解会忽略该字段


@ExcelIgnoreUnannotated

默认不加ExcelProperty 的注解的都会参与读写,加了不会参与读写


@DateTimeFormat

日期转换,用String去接收excel日期格式的数据会调用这个注解,参数如下:

名称

默认值

描述

value

参照java.text.SimpleDateFormat书写即可

use1904windowing

自动选择

excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始


@NumberFormat

数字转换,用String去接收excel数字格式的数据会调用这个注解。

名称

默认值

描述

value

参照java.text.DecimalFormat书写即可

roundingMode

RoundingMode.HALF_UP

格式化的时候设置舍入模


@ColumnWidth

用于设置表格列的宽度(value = 20);

五、版本选择

版本

poi依赖版本 (支持范围)

jdk版本支持范围

备注

3.1.0+

4.1.2 (4.1.2 - 5.2.2)

jkd8 - jdk17

推荐使用,会更新的版本

3.0.0-beta1 - 3.0.5

4.1.2 (4.1.2 - 5.2.2)

jkd8 - jdk11

不推荐项目新引入此版本,除非超级严重bug,否则不再更新

2.0.0-beta1-2.2.11

3.17 (3.17 - 4.1.2)

jdk6 - jdk11

不推荐项目新引入此版本,除非是jdk6否则不推荐使用,除非超级严重bug,否则不再更新

1+版本

3.17 (3.17 - 4.1.2)

jdk6 - jdk11

不推荐项目新引入此版本,超级严重bug,也不再更新


六、使用示例

1. 读Excel

对象

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    /**
     * 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
     */
    @ExcelProperty(index = 2)
    private Double doubleData;
    
    /**
     * 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
     */
    @ExcelProperty("字符串标题")
    private String string;
    
    @ExcelProperty("日期标题")
    private Date date;
}

监听器

// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
public class DemoDataListener implements ReadListener<DemoData> {


    /**
     * 每隔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("存储数据库成功!");
    }
}

代码

    /**
     * 指定列的下标或者列名
     *
     * <p>1. 创建excel对应的实体对象,并使用{@link ExcelProperty}注解. 参照{@link IndexOrNameData}
     * <p>2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link IndexOrNameDataListener}
     * <p>3. 直接读即可
     */
    @Test
    public void indexOrNameRead() {
        String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
        // 这里默认读取第一个sheet
        EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
    }


2. 写Excel

对象

@Getter
@Setter
@EqualsAndHashCode
public class DemoData {
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
    /**
     * 忽略这个字段
     */
    @ExcelIgnore
    private String ignore;
}

代码

    /**
     * 最简单的写
     * <p>
     * 1. 创建excel对应的实体对象 参照{@link DemoData}
     * <p>
     * 2. 直接写即可
     */
    @Test
    public void simpleWrite() {
        // 注意 simpleWrite在数据量不大的情况下可以使用(5000以内,具体也要看实际情况),数据量大参照 重复多次写入


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


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


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


七、其他说明

3+版本的的easyexcel,使用poi 5+版本时,需要自己引入poi 5+版本的包,且手动排除:poi-ooxml-schemas,例如:



<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.1.0</version>
    <exclusions>
        <exclusion>
            <artifactId>poi-ooxml-schemas</artifactId>
            <groupId>org.apache.poi</groupId>
        </exclusion>
    </exclusions>
</dependency>


八、版本升级

  • 不建议跨大版本升级 尤其跨2个大版本
  • 大版本升级后建议相关内容重新测试下
  • 2+ 升级到 3+ ,下面3个地方不兼容:
  • 使用了自定义拦截器去修改样式的会出问题(不会编译报错)

// 以前的写法
@Override
protected void setHeadCellStyle(Cell cell, Head head, Integer relativeRowIndex) {
            cell.setCellStyle(style);
 }


// 现在的写法1
// 这个写完也需要测试下 还是老代码 不管使用了什么拦截器 都可以这么写 
// 这个会导致格式化数据失效
    protected void setHeadCellStyle(CellWriteHandlerContext context) {
            cell.setCellStyle(style);


         // 这里要把 WriteCellData的样式清空, 不然后面还有一个拦截器 FillStyleCellWriteHandler 默认会将 WriteCellStyle 设置到
         // cell里面去 会导致自己设置的不一样
         context.getFirstCellData().setWriteCellStyle(null);
    }




// 现在的写法2 推荐
// 这个方案靠谱 以前用 poi的CellStyle  现在用 WriteCellStyle 入参基本都一致
    protected void setHeadCellStyle(CellWriteHandlerContext context) {
                     // 第一个单元格
                        // 只要不是头 一定会有数据 当然fill的情况 可能要context.getCellDataList() ,这个需要看模板,因为一个单元格会有多个 WriteCellData
                        WriteCellData<?> cellData = context.getFirstCellData();
                        // 这里需要去cellData 获取样式
                        // 很重要的一个原因是 WriteCellStyle 和 dataFormatData绑定的 简单的说 比如你加了 DateTimeFormat
                        // ,已经将writeCellStyle里面的dataFormatData 改了 如果你自己new了一个WriteCellStyle,可能注解的样式就失效了
                        // 然后 getOrCreateStyle 用于返回一个样式,如果为空,则创建一个后返回
                        WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
                        writeCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
                        // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
                        writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
    }
  • 读的时候invoke里面抛出异常,不会再额外封装一层ExcelAnalysisException (不会编译报错)这个捕获异常的时候 不用再getCause了
  • 样式等注解涉及到 boolean or 一些枚举 值的 有变动,新增默认值(会编译报错,注解改就行)这个直接改了就行



相关推荐

作为后端开发,你知道MyBatis有哪些隐藏的 “宝藏” 扩展点吗?

在互联网大厂后端开发领域,MyBatis作为一款主流的持久层框架,凭借其灵活的配置与强大的数据处理能力,广泛应用于各类项目之中。然而,随着业务场景日趋复杂、系统规模不断扩张,开发过程中常面临SQL...

基于Spring+SpringMVC+Mybatis分布式敏捷开发系统架构(附源码)

前言zheng项目不仅仅是一个开发架构,而是努力打造一套从前端模板-基础框架-分布式架构-开源项目-持续集成-自动化部署-系统监测-无缝升级的全方位J2EE企业级开发解...

基于Java实现,支持在线发布API接口读取数据库,有哪些工具?

基于java实现,不需要编辑就能发布api接口的,有哪些工具、平台?还能一键发布、快速授权和开放提供给第三方请求调用接口的解决方案。架构方案设计:以下是一些基于Java实现的无需编辑或只需少量编辑...

Mybatis Plus框架学习指南-第三节内容

自动填充字段基本概念MyBatis-Plus提供了一个便捷的自动填充功能,用于在插入或更新数据时自动填充某些字段,如创建时间、更新时间等。原理...

被你误删了的代码,在 IntelliJ IDEA中怎么被恢复

在IntelliJIDEA中一不小心将你本地代码给覆盖了,这个时候,你ctrl+z无效的时候,是不是有点小激动?我今天在用插件mybatisgenerator自动生成mapper的时候,...

修改 mybatis-generator 中数据库类型和 Java 类型的映射关系

使用mybatis-generator发现数据库类型是tinyint(4),生成model时字段类型是Byte,使用的时候有点不便数据库的类型和Model中Java类型的关系...

又被问到了, java 面试题:反射的实现原理及用途?

一、反射的实现原理反射(Reflection)是Java在运行时动态获取类的元数据(如方法、字段、构造器等)并操作类对象的能力。其核心依赖于...

Spring Boot 中JPA和MyBatis技术那个更好?

你在进行SpringBoot项目开发时,是不是也经常在选择JPA和MyBatis这两个持久化技术上犯难?面对众多前辈的经验之谈,却始终拿不准哪种技术才最适合自己的项目?别担心,今天咱们就...

Spring Boot (七)MyBatis代码自动生成和辅助插件

一、简介1.1MyBatisGenerator介绍MyBatisGenerator是MyBatis官方出品的一款,用来自动生成MyBatis的mapper、dao、entity的框架,让...

解决MyBatis Generator自动生成.java.1文件

MyBatis框架操作数据库,一张表对应着一个实体类、一个Mapper接口文件、一个Mapper映射文件。一个工程项目通常最少也要几十张表,那工作量可想而知非常巨大的,MyBatis框架替我们想好了解...

Linux yq 命令使用详解

简介yq是一个轻量级、可移植的命令行...

7 段不到 50 行的 Python 脚本,解决 7 个真实麻烦:代码、场景与可复制

“...

Python学不会来打我(62) json数据操作汇总

很多小伙伴学了很久的python一直还是没有把数据类型之间的转换搞明白,上一篇文章我们详细分享了python的列表、元组、字典、集合之间的相互转换,这一篇文章我们来分享json数据相关的操作,虽然严格...

之前3W买的Python全系列教程完整版(懂中文就能学会)

今天给大家带来了干货,Python入门教程完整版,完整版啊!完整版!言归正传,小编该给大家介绍一下这套教程了,希望每个小伙伴都沉迷学习,无法自拔...

x-cmd pkg | grex - 正则表达式生成利器,解决手动编写的烦恼

简介grex是一个旨在简化创作正则表达式的复杂且繁琐任务的库和命令行程序。这个项目最初是DevonGovett编写的JavaScript工具regexgen的Rust移植。但re...

取消回复欢迎 发表评论: