文章目录
一、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.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,是实际开发中这三个是最常用的工具
- 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;
}
}
- 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;
}
}
- 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数据及导出后的效果参照下图:
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);