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 |
不支持 | - | - |
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);
}
}
完整代码