POI 的 API 較複雜,而且低版本有OOM的風險。
快節奏的互聯網時代,使開發效率成了我們技術人追求。
這裏帶大家十分鐘入門 alibaba/easyexcel
Writer
-
head 方法支持傳入參數的格式
public ExcelWriterBuilder head(Class clazz)
public ExcelWriterBuilder head(List<List<String>> head)
-
doWrite 方法支持傳入參數的格式
public void doWrite(List<Class> data)
public void doWrite(List<List<Object>> data)
使用對象寫
/**
* 最簡單的寫 使用定義的對象寫
* <p>1. 創建excel對應的實體對象 參照{@link WriteData}
* <p>2. 獲取寫入的數據 List<Class>
* <p>3. 直接寫即可
*/
@Test
public void simpleWrite() {
// 這裏 需要指定寫用哪個class去寫,然後寫到第一個sheet,名字爲模板 然後文件流會自動關閉
// 如果這裏想使用03 則 傳入excelType參數即可
EasyExcel.write("./test.xlsx", WriteData.class)
.excelType(ExcelTypeEnum.XLSX)
.sheet("demoData")
.doWrite(data());
}
WriteData.java
@Data
@Builder
@HeadRowHeight(25)
@ContentRowHeight(20)
@ColumnWidth(30)
public class WriteData {
@ExcelProperty(value = "字符串標題", converter = CustomTitleConverter.class)
private String string;
@DateTimeFormat("yyyy年MM月dd日 HH時mm分ss秒")
@ExcelProperty(value = "日期標題")
private Date date;
@ExcelProperty("數字標題")
private Double doubleData;
@ExcelProperty(value = "禁用標誌", converter = CustomDisabledConverter.class)
private Boolean disabled;
/**
* 忽略這個字段
*/
@ExcelIgnore
private String ignore;
}
使用 list 寫
/**
* 使用list寫
* <p>1. 定義List<List<String>>結構的表頭
* <p>2. 定義List<List<Object>>結構的數據
* <p>3. 寫數據
*/
@Test
public void listWrite() {
List<List<String>> head = new ArrayList<>();
head.add(Arrays.asList("head1"));
head.add(Arrays.asList("head2"));
head.add(Arrays.asList("head3"));
head.add(Arrays.asList("head4"));
List<List<Object>> data = new ArrayList<>();
for (int i = 0; i < 10; i++) {
data.add(Arrays.asList("字符串" + i, new Date(), 0.56, (i % 2 == 0)));
}
EasyExcel.write("./test.xlsx")
.excelType(ExcelTypeEnum.XLSX)
.head(head)
.sheet("demoData")
.doWrite(data);
}
Reader
讀一個sheet
/**
* 異步讀
* <p>
* 1. 由於默認異步讀取excel,所以需要創建excel一行一行的回調監聽器,參照{@link ObjectListener}
* <p>
* 2. 直接讀即可
*/
@Test
public void read() {
ObjectListener listener = new ObjectListener();
// 這裏 需要指定監聽器用於存儲數據,然後讀取第一個sheet 文件流會自動關閉
EasyExcel.read("./test.xlsx", listener)
// 0 是第一個sheet的index
.sheet(0)
// 同步讀 doReadSync()
// 異步讀 doRead()
.doRead();
System.out.println(listener.getList().stream().count());
listener.getList().stream().forEach(System.out::println);
// 2
// {0=自定義:字符串0, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=可用}
// {0=自定義:字符串1, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=禁用}
}
輸出
2
{0=自定義:字符串0, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=可用}
{0=自定義:字符串1, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=禁用}
讀全部sheet
/**
* @description: 讀取所有的sheet數據
* @author: tanpeng
* @date : 2020-02-13 10:50
* @version: v1.0.0
*/
@Test
public void readAll() {
ObjectListener listener = new ObjectListener();
// 這裏 需要指定監聽器用於存儲數據,然後讀取第一個sheet 文件流會自動關閉
EasyExcel.read("./test.xlsx", listener)
.doReadAll();
System.out.println(listener.getList().stream().count());
listener.getList().stream().forEach(System.out::println);
}
輸出
4
{0=自定義:字符串0, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=可用}
{0=自定義:字符串1, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=禁用}
{0=自定義:字符串0, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=可用}
{0=自定義:字符串1, 1=2020年02月13日 09時32分29秒, 2=0.56, 3=禁用}
Listener
正確使用 Listener
invoke
數據校驗、數據轉換等工作doAfterAllAnalysed
整個excel解析結束時執行
/**
* 解析監聽器,
* 每解析一行會回調invoke()方法。
* 整個excel解析結束會執行doAfterAllAnalysed()方法
* @description:
* @author: tanpeng
* @date: 2020-02-07 14:55
* @version: v1.0.0
*/
@Data
public class ConverterDataListener extends AnalysisEventListener<ConverterData> {
@Override
public void invoke(ConverterData data, AnalysisContext context) {
// 數據校驗、數據轉換等工作
if (data != null) {
if (data.getString().contains("5")) {
// 不處理本條數據
// return;
// 直接結束數據處理,不再處理後面的數據 throw new RuntimeException()
throw new RuntimeException("包含數字5");
} else {
// 處理數據
System.out.println(data.toString());
}
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 處理完畢
System.out.println("done");
}
}
錯誤使用 Listener
新同學很容易,會像下面的代碼一樣使用ObjectListener
,聲明一個空的list
容器用來保存invoke
中返回的每條數據,這樣其實又是將數據放在了內存中進行計算,當解析大文件時,同樣會OOM
實際上easyexcel
使用 SAX 模式解析,每次只返回一條數據。
返回的這一條數據就是invoke
方法的第一個參數,在invoke
方法中應該做數據校驗、數據轉換
等工作,而不是將數據又逐條往內存裏放,放到最後說不定又到了OOM的局面。
/**
* 解析監聽器,
* 每解析一行會回調invoke()方法。
* 整個excel解析結束會執行doAfterAllAnalysed()方法
* @description:
* @author: tanpeng
* @date: 2020-02-07 13:00
* @version: v1.0.0
*/
@Data
public class ObjectListener extends AnalysisEventListener<Object> {
/**
* @description: 默認範型爲 LinkedHashMap<Integer, String> {@link MapCache}
* @author: tanpeng
* @date : 2020-02-07 14:33
* @version: v1.0.0
*/
private List<Object> list = new ArrayList<>();
@Override
public void invoke(Object data, AnalysisContext context) {
if (data != null) {
list.add(data);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}
工作原理
easyexcel 的工作原理圖,它清楚的告訴了你爲什麼使用easyexcel
可以避免OOM
- 1、文件解壓文件讀取通過文件形式
- 2、避免將全部全部數據一次加載到內存
- 3、拋棄不重要的數據
Excel解析時候會包含樣式,字體,寬度等數據,但這些數據是我們不關心的,如果將這部分數據拋棄可以大大降低內存使用。Excel中數據如下Style佔了相當大的空間。
工程結構
其他
實體類可使用的註解
Excel 限制
版本 | 最大行 | 最大列 |
---|---|---|
Excel 2003 .xls | 65536 | 256 |
Excel 2007 .xlsx | 1048576 | 16384 |
不支持csv 的讀寫 |
- | - |
CellDataTypeEnum枚舉類 學習
枚舉類 可以看作是關係型數據庫中的一張表
package com.alibaba.excel.enums;
import java.util.HashMap;
import java.util.Map;
import com.alibaba.excel.util.StringUtils;
/**
* excel internal data type
*
* @author Jiaju Zhuang
*/
public enum CellDataTypeEnum {
/**
* string
*/
STRING,
/**
* This type of data does not need to be read in the 'sharedStrings.xml', it is only used for overuse, and the data
* will be stored as a {@link #STRING}
*/
DIRECT_STRING,
/**
* number
*/
NUMBER,
/**
* boolean
*/
BOOLEAN,
/**
* empty
*/
EMPTY,
/**
* error
*/
ERROR,
/**
* Images are currently supported only when writing
*/
IMAGE;
private static final Map<String, CellDataTypeEnum> TYPE_ROUTING_MAP = new HashMap<String, CellDataTypeEnum>(16);
static {
TYPE_ROUTING_MAP.put("s", STRING);
TYPE_ROUTING_MAP.put("str", DIRECT_STRING);
TYPE_ROUTING_MAP.put("inlineStr", STRING);
TYPE_ROUTING_MAP.put("e", ERROR);
TYPE_ROUTING_MAP.put("b", BOOLEAN);
TYPE_ROUTING_MAP.put("n", NUMBER);
}
/**
* Build data types
*
* @param cellType
* @return
*/
public static CellDataTypeEnum buildFromCellType(String cellType) {
if (StringUtils.isEmpty(cellType)) {
return EMPTY;
}
return TYPE_ROUTING_MAP.get(cellType);
}
}