EasyExcel详解

文章目录

一、easyExcel

1.什么是easyExcel

内容摘自官方: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模式,在上层做了模型转换的封装,让使用者更加简单方便

通俗解释就是说:一个基于poi的excel简化开发包,性能比poi要好,且易于使用

官方文档地址
源码地址

2.easyExcel示例demo

官方文档非常全面,本无需写一个demo来记录。本demo旨在展示easyExcel的读写基础用法、自定义类型转换、自定义单元格格式及excel空白行处理等,可以理解为将常用的情况记录下来,省去看官方文档的时间。

java 复制代码
## PersonVO.class,代码中的Person.class和PersonVO.class的区别为没有ifOffer字段,为了展示而做了区分
## Person.class是用来读excel的,PersonVO.class用来写excel
@Data
public class PersonVo {

    @ExcelProperty("名称")
    private String name;

    @ExcelProperty("性别")
    private String gender;

    @ExcelProperty("年龄")
    private Integer age;

    @ExcelProperty("信息")
    private String info;

    @ExcelProperty("评分")
    private Float score;

	// OfferEnumConverter为自定义的Converter,用来做OfferEnum和String的映射
    @ExcelProperty(value = "是否录用", converter = OfferEnumConverter.class)
    private OfferEnum ifOffer;
}


## excel读及写部分,如果read时使用PersonVo.class映射表头
## 则可以在CustomPageReadListener.class的invoke方法中,做对person.ifOffer的赋值

File file = new File("D:\\develop\\work\\test.xlsx");
try (InputStream is = Files.newInputStream(file.toPath())) {
    // 读取数据
    List<PersonVo> excelDatas = new ArrayList<>();
    EasyExcel.read(is, Person.class, new CustomPageReadListener<Person>(dataList -> {
        if (CollectionUtils.isEmpty(dataList)) {
            return;
        }
        dataList.forEach(data -> {
            PersonVo personVo = new PersonVo();
            BeanUtils.copyProperties(data, personVo);
            excelDatas.add(personVo);
        });
    })).sheet().doReadSync();

	// 为了实现自定义表格样式,根据ifOffer来决定行颜色
    Map<Integer, Short> cellColorType = new HashMap<>();
    for (int i = 0; i < excelDatas.size(); i++) {
        PersonVo person = excelDatas.get(i);
        if (person.getScore() > 3) {
            person.setIfOffer(OfferEnum.OFFER);
            cellColorType.put(i + 1, IndexedColors.GREEN.getIndex());
        } else if (person.getScore() < 2) {
            person.setIfOffer(OfferEnum.REFUSE);
            cellColorType.put(i + 1, IndexedColors.RED.getIndex());
        } else {
            person.setIfOffer(OfferEnum.WAIT);
            cellColorType.put(i + 1, IndexedColors.YELLOW.getIndex());
        }
    }

    EasyExcel.write("D:\\develop\\work\\test1.xlsx", PersonVo.class)
            .registerWriteHandler(new CustomCellWriteHandler(cellColorType))
            .sheet("测试")
            .doWrite(excelDatas);
} catch (IOException e) {
    throw new RuntimeException(e);
}

demo中用到了自定义类型转换OfferEnumConverter 、自定义excel读取监听器CustomPageReadListener 、自定义WriteHandler CustomCellWriteHandler,是实际开发中这三个是最常用的工具

  1. OfferEnumConverter: String <--> Enum转换器,实现supportJavaTypeKey及supportExcelTypeKey是为了在Easy.registerConverter()注册通用转换器也可以使用
java 复制代码
## OfferEnumConverter.class
public class OfferEnumConverter implements Converter<OfferEnum> {

    @Override
    public Class<OfferEnum> supportJavaTypeKey() {
        return OfferEnum.class;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return CellDataTypeEnum.STRING;
    }

    @Override
    public OfferEnum convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return OfferEnum.valueOf(cellData.getStringValue());
    }

    @Override
    public WriteCellData<?> convertToExcelData(OfferEnum value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        if(Objects.isNull(value)) {
            return new WriteCellData<>("");
        } else {
            return new WriteCellData<>(value.getValue());
        }
    }
}

## OfferEnum.class 录用标记枚举
@Getter
public enum OfferEnum {

    OFFER("y", "录用"),
    REFUSE("n", "不录用"),
    WAIT("wait", "待定");;

    private final String value;

    private final String desc;

    OfferEnum(String value, String desc) {
        this.value = value;
        this.desc = desc;
    }
    
    public static OfferEnum getByValue(String value) {
        for (OfferEnum offerEnum : OfferEnum.values()) {
            if (offerEnum.value.equals(value)) {
                return offerEnum;
            }
        }
        return WAIT;
    }
}
  1. CustomPageReadListener: 监听器是在读取完一行数据后被调用的,invoke中接收到的是一行的数据。这里做了处理空行的操作,虽然EasyExcel默认情况下会配置ignoreEmptyRow为true,但是如果行内某个单元格无数据但有单元格式,会被EasyExcel认为非空行,因此对空行严谨的项目需要在这里处理一下空行。
java 复制代码
public class CustomPageReadListener<T> extends PageReadListener<T> {
    public CustomPageReadListener(Consumer<List<T>> consumer) {
        super(consumer);
    }

    @Override
    public void invoke(T data, AnalysisContext context) {
        // 处理空行
        if (isNullLine(data)) {
            return;
        }
        // 特殊字段赋值及处理(如:dateStr赋值给date)
        flushData(data);
        // 处理数据转换异常
        super.invoke(data, context);
    }

    private void flushData(T data) {

    }

    private boolean isNullLine(T data) {
        System.err.println(JSON.toJSONString(data));
        // 获取data每个字段,反射判断是不是都为空或空字符串
        for (Field field : data.getClass().getDeclaredFields()) {
            field.setAccessible(true);
            try {
                Object value = field.get(data);
                if (value instanceof String) {
                    if (!StringUtils.isEmpty(value)) {
                        return false;
                    }
                } else {
                    if (Objects.nonNull(value)) {
                        return false;
                    }
                }
            } catch (IllegalAccessException e) {
                return false;
            }
        }
        return true;
    }
}
  1. CustomCellWriteHandler: 将内存中的数据写入excel时,需要做一些特殊处理时(如:脱敏处理、添加单元格样式、合并单元格等),可以通过实现WriteHandler来实现功能,demo中只有添加单元格样式,官方文档中有很全面的各种案例用法
java 复制代码
public class CustomCellWriteHandler implements CellWriteHandler {

    private final Map<Integer, Short> cellColorType;
    public CustomCellWriteHandler(Map<Integer, Short> cellColorType) {
        if(Objects.isNull(cellColorType)) {
            cellColorType = new HashMap<>();
        }
        this.cellColorType = cellColorType;
    }

    @Override
    public void afterCellDispose(CellWriteHandlerContext context) {
    	// 表头样式不变
        if (BooleanUtils.isNotTrue(context.getHead())) {

            int rowIndex = context.getRowIndex();
            Short colorIndex = cellColorType.get(rowIndex);
            if(Objects.nonNull(colorIndex)) {
                WriteCellData<?> cellData = context.getFirstCellData();
                // 这里需要去cellData 获取样式
                // 很重要的一个原因是 WriteCellStyle 和 dataFormatData绑定的 简单的说 比如你加了 DateTimeFormat
                // ,已经将writeCellStyle里面的dataFormatData 改了 如果你自己new了一个WriteCellStyle,可能注解的样式就失效了
                // 然后 getOrCreateStyle 用于返回一个样式,如果为空,则创建一个后返回
                WriteCellStyle writeCellStyle = cellData.getOrCreateStyle();
                writeCellStyle.setFillForegroundColor(colorIndex);
                // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND
                writeCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
            }
        }
    }
}

本案例使用的test.excel数据及导出后的效果参照下图:

3.easyExcel read的底层逻辑

通过ExcelAnalyser来配置excel解析执行器

  • 通过FileMagic来读取文件开头几个字节的魔数,以确定文件的类型。为了兼容CSV文件,通过File方式readExcel的时候,通过判断文件的后缀名称是否为.csv来判断是否为CSV文件
  • 设置read上下文:解析表头,加载readListener、Converter(预定义的Converter和通过registerConverter注册的Converter)、设置忽略空行(如果空行中有表格样式,则无法忽略)及readCache
  • 设置read执行器:选择合适的执行器,并加载所有的sheet。这里加载了所有的sheet,在read的时候会根据条件选择要读取的sheet

通过ExcelAnalyser.analysis来解析excel

  • 从xlsx视角出发的,xls和csv这里不做展示
  • XlsxSaxAnalyser.parseXmlSource()中使用SAXParserFactory来解析 Excel 文件底层 XML 结构。SAXParserFactory基于 SAX(Simple API for XML)事件驱动模型实现高效的大文件流式解析,避免内存溢出(OOM)
  • XlsxRowHandler重写了startElement来实现对每一行每一个单元格的读取。当所有XlsxTagHandler执行完后,开始endElement进行cell类型的转换等,最终交给AnalysisEventProcessor.endRow来处理数据,并调用ReadListener监听器来对数据做处理(如PageReadListener来缓存数据)
  • EasyExcel有四个解析excel的入口,分别为
    • .sheet().doRead() -- sheet中不加参数,则默认取sheetNo为0的sheet,doRead中进行解析excel
    • .sheet().doReadSync() -- 相对doRead(),注册了一个新的Listener用来缓存数据,读取excel结束后直接从Listner中读取数据并return
    • doReadAll() -- 顾名思义,读取所有的sheet(),并映射到同一个实体list中,适合同类型分页数据
    • .doReadAllSync() -- 同上
  • 读取excel的关键为SAXParserFactory和ReadCache,具体逻辑可以自己阅读源码,或使用AI工具辅助阅读

4.easyExcel write的底层逻辑


二、FastExcel

文本采用的fastExcel版本为1.0.0,当前时间最新版本为1.2.0

目前FastExcel官网已挂,仅有开源源码地址

1.为什么更换为fastExcel

  • 2024年8月阿里已宣布停止更新easyExcel,同时原作者宣布新开发fastExcel,支持所有easyExcel的功能,因此原easyExcel用户可以最低成本过度到fastExcel
  • fastExcel通过对底层算法的优化和内存管理的改进,能更高效的处理大规模的excel数据,大幅降低内存消耗和处理时间
  • 新功能:读取excel指定行数,excel转pdf(注意:仅仅是将excel文件转为pdf文件,且在1.1.0版本中已经移除此功能,谨慎使用)

2.fastExcel新功能

java 复制代码
## fastExcel中既可以用FastExcel.class,也可以用EasyExcel.class,除了1.0.0版本外,俩完全一样
## .numRows()即读取excel指定行数,.numRows(10)即从表头开始读10行,上文中的案例,就只会读到9条数据
FastExcel.read(is, Person.class, new PageReadListener<Person>(dataList -> {
    if (CollectionUtils.isEmpty(dataList)) {
        return;
    }
    dataList.forEach(data -> {
        PersonVo personVo = new PersonVo();
        BeanUtils.copyProperties(data, personVo);
        excelDatas.add(personVo);
    });
})).sheet().numRows(10).doRead();

## excel文件转为pdf文件,谨慎使用
FastExcel.convertToPdf(new File("D:\\develop\\work\\test1.xlsx"), new File("D:\\develop\\work\\test2.pdf"), null, null);
相关推荐
小信丶几秒前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_5 分钟前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神5 分钟前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe9 分钟前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿9 分钟前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记20 分钟前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson21 分钟前
CAS的底层实现
java
九英里路32 分钟前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串
YDS82937 分钟前
大营销平台 —— 抽奖前置规则过滤
java·spring boot·ddd
仍然.41 分钟前
多线程---CAS,JUC组件和线程安全的集合类
java·开发语言