EasyExcel详解

发布于:2025-05-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、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空白行处理等,可以理解为将常用的情况记录下来,省去看官方文档的时间。

## PersonVO.class,代码中的Person.classPersonVO.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()注册通用转换器也可以使用
## 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认为非空行,因此对空行严谨的项目需要在这里处理一下空行。
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中只有添加单元格样式,官方文档中有很全面的各种案例用法
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数据及导出后的效果参照下图:
Excel测试数据
WriteExcel结果

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新功能

## 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);


网站公告

今日签到

点亮在社区的每一天
去签到