文章目錄
- 一、easyExcel
- 1.什么是easyExcel
- 2.easyExcel示例demo
- 3.easyExcel read的底層邏輯
- ~~4.easyExcel write的底層邏輯~~
- 二、FastExcel
- 1.為什么更換為fastExcel
- 2.fastExcel新功能
一、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> {@Overridepublic Class<OfferEnum> supportJavaTypeKey() {return OfferEnum.class;}@Overridepublic CellDataTypeEnum supportExcelTypeKey() {return CellDataTypeEnum.STRING;}@Overridepublic OfferEnum convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {return OfferEnum.valueOf(cellData.getStringValue());}@Overridepublic 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);}@Overridepublic 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;}@Overridepublic 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_FOREGROUNDwriteCellStyle.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);