在 Java
中有两个较为常用的 Excel
类库,分别是 Apache
的 POI
以及 Alibaba
的 EasyExcel
,其中 EasyExcel
引入了事件驱动模型在内存占用方面有着优秀的表现。
在这篇文章中将介绍如何通过
EasyExcel
对Excel
文件进行读写操作。
一、基础读取
1. 模拟数据
首先先准备一个 Excel
数据文件,文件内容如下:
id name gender birthday
1 User-1 Male 2024-01-29
2 User-2 Male 2024-01-29
3 User-3 Male 2024-01-29
4 User-4 Male 2024-01-29
完成后,为上述的数据创建对应的实体类对象,这里省去了 get
和 set
方法部分内容。
public class User {
private String id;
private String name;
private String gender;
private Date birthday;
}
2. 数据读取
读取数据的方式十分简单,通过 EasyExcel
对象即可创建读取对象,其可配置的内容参考下表。
方法 | 作用 |
---|---|
read() | 读取的对象,传入类型可为 File, IO 或 文件路径。 |
head() | 指定 Excel 列,当为类时则与类属性名称一一对应。 |
sheet() | 指定 Sheet 页,默认为第一页,可通过名称或下标指定。 |
headRowNumber() | 指定文件表头所在的行数,默认为 1。 |
需要注意的是在使用 head(xxx.class)
时默认数据字段映射关系是与类属性定义顺序一致的。
以刚才创建的 User
类为例,Excel
文件中列的定义顺序为 id, name, gender, birthday
,则在赋值时其默认实体类中第一个属性即为 id
并以此类推。如果此时 User
的属性定义顺序为 name, id
那么最终读取的数据 id
值将会被赋值给 name
字段。
下述示例代码即为通过 EasyExcel
读取文件数据。
public class ExcelReadTest {
@Test
public void read() {
String fileLocate = "src\\main\\resources\\data.xls";
try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
List<User> list = EasyExcel.read(in)
.head(User.class)
.sheet()
.doReadSync();
System.out.println(list);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3. 动态读取
EasyExcel
除了支持实体类映射外还可以动态指定列名实现读取。
在 head()
中通过集合动态指定文件中的列名,其返回的结果为 List<LinkedMap<Integer, Object>>
,其中每一个 LinkMap
对应文件内的一行数据,且 Key
为对应列的下表,Value
为对应单元格的数据。
public void read() {
String fileLocate = "src\\main\\resources\\data.xls";
try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
// 定义列名
List<List<String>> heads = Stream.of("id", "name", "gender", "birthday")
.map(Arrays::asList)
.collect(Collectors.toList());
List<LinkedMap<Integer, Object>> list = EasyExcel.read(in)
.head(heads)
.sheet()
.doReadSync();
System.out.println(list);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
二、数据写入
1. 写入示例
EasyExcel
写入操作与读取类似,其中的 excelType()
用于指定文件类型,可选值有下述三类,对应三种不同的数据文件格式。
ExcelTypeEnum.CSV
ExcelTypeEnum.XLS
ExcelTypeEnum.XLSX
数据写入的方式与之前的读取示例类似,不同之处仅为之前的 read()
与 doReadSync()
替换为 write()
与 doWrite()
,其余的属性配置不变,示例代码如下:
public void demo1() {
List<User> dataList = new ArrayList<>();
for (int i = 1; i < 5; i++) {
dataList.add(new User(i + "", "User-" + i, "Male", new Date()));
}
String fileLocate = "src\\main\\resources\\data.xls";
EasyExcel.write(fileLocate)
.excelType(ExcelTypeEnum.XLS)
.head(User.class)
.sheet("Test-data")
.doWrite(data);
}
2. Sheet管理
当需要写入多个 Sheet
到同一个 Excel
文件时,即可使用 ExcelWriterSheetBuilder
。
如下述示例中即创建两个 Sheet
页数据,Sheet
页名称分别为 Sheet-1
和 Sheet-2
。
public void demo2() throws IOException {
List<User> dataList = new ArrayList<>();
for (int i = 1; i < 5; i++) {
dataList.add(new User(i + "", "User-" + i, "Male", new Date()));
}
String fileLocate = "src\\main\\resources\\info.xls";
// 创建 ExcelWriter 对象
ExcelWriter excelWriter = EasyExcel.write(fileLocate)
.excelType(ExcelTypeEnum.XLS)
.build();
for (int i = 1; i <= 2; i++) {
// 创建 Sheet 对象并写入数据
ExcelWriterSheetBuilder sheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
sheetBuilder.sheetName("Sheet-" + i);
sheetBuilder.head(User.class);
sheetBuilder.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy());
// 将 sheet 页写入 excel 中
excelWriter.write(dataList, sheetBuilder.build());
}
// 写入完成
excelWriter.finish();
}
3. 动态写入
在上述的写入示例中,文件的数据的写入和列名的定义都是通过 Java
实体对象映射实现。而 EasyExcel
也提供了动态的写入方式,通过 List
集合传入可变对象,由此即可为系统设计一套通用的数据写入模块。
在动态写入中,Excel
文件的列名通过 List<List<String>>
格式数据指定,而数据的载体是 List<List<Object>>
而非动态读取示例中的 List<Map>
。
// 动态读取返回的数据格式
// List<Map<String, Object>>
[
{
"id": "1",
"name": "user-1",
"gender": "Male",
"birthday": "2023-01-01 00:00:01"
}, {
"id": "2",
"name": "user-2",
"gender": "Male",
"birthday": "2023-01-01 00:00:02"
}
]
// 动态写入要求的数据格式
// List<List<Object>>
[
["1", "user-1", "Male", "2023-01-01 00:00:01"],
["2", "user-2", "Male", "2023-01-01 00:00:02"]
]
根据上述逻辑,其相对应的程序代码如下:
public void dynamicWrite() throws IOException {
// 构建测试数据
List<Map<String, Object>> dataList = new ArrayList<>();
for (int i = 1; i < 5; i++) {
Map<String, Object> map = Map.of(
"id", i + "",
"name", "user-" + i,
"gender", "Male",
"birthday", new Date()
);
dataList.add(map)
}
List<String> headList = List.of("id", "name", "gender", "birthday");
// 转化 "List<Map>" 为 "List<List>"
List<List<Object>> rowDatas = new ArrayList<>();
for (Map<String, Object> map : dataList) {
List<Object> row = new ArrayList<>();
for (String name : headList) {
row.add(map.get(name));
}
rowDatas.add(row);
}
// 声明 Excel 的表头
List<List<String>> heads = headList.stream()
.map(Arrays::asList)
.collect(Collectors.toList());
// 写入数据
String fileLocate = "src\\main\\resources\\info.xls";
EasyExcel.write(fileLocate)
.excelType(ExcelTypeEnum.XLS)
.head(heads)
.sheet("Test-data")
.doWrite(rowDatas);
}
三、注解使用
上面的读写示例中都是采用默认的实体类属性映射方式,但 EasyExcel
提供了更灵活的注解用于字段映射,下面将分别进行介绍。
1. ExcelProperty
@ExcelProperty
注解存在 value
与 order
两个属性,前者用于设置别名而后者用于设置列顺序。
通过此属性即可指定列与实体字段的映射关系,同时默认的字段的映射顺序是与属性的定义顺序相关,因此通过 order
属性可灵活的修改映射顺序。
如下示例中则最后生成 Excel
文件中列名分别为 编号
与 姓名
,其中第一列为 编号
,第二列为 姓名
。
public class ExcelUser {
@ExcelProperty(value = "编号", order = 1)
private String id;
@ExcelProperty(value = "姓名", order = 2)
private String name;
}
2. ExcelIgnore
@ExcelIgnore
用于指定需要忽略的属性,由于 EasyExcel 是与类的属性进行映射,如若实体的属性字段数量与文件表头数量不一致导致映射失败。而在实际开发场景中,在数据类中常会涉及到某些不相关的业务字段,此时即通过 @ExcelIgnore
标注为忽略。
如下示例在读取或写入文件时将忽略 address
字段。
public class ExcelUser {
private String id;
@ExcelIgnore
private Date address;
}
3. DateTimeFormat
@DateTimeFormat
用于指定读取数据的时间格式化形式。
如下示例在读取或写入文件时将会格式化字段 birthday
字段 值为 yyy-MM-dd
格式。
public class ExcelUser {
private String id;
@DateTimeFormat("yyy-MM-dd")
private Date birthday;
}
4. 应用示例
下面通过示例演示注解的具体应用方式。
同样的创建的 User
实体类,通过 @ExcelProperty
注解此时则无需担心属性的定义顺序,而是可以通过 order
指定顺序。
public class User {
@ExcelProperty(value = "生日", order = 4)
@DateTimeFormat("yyy-MM-dd")
private Date birthday;
@ExcelProperty(value = "性别", order = 3)
private String gender;
@ExcelProperty(value = "姓名", order = 2)
private String name;
@ExcelProperty(value = "编号", order = 1)
private String id;
}
类似的,准备一份 Excel
文件,数据内容如下:
编号 姓名 性别 生日
1 User-1 Male 2024-01-29
2 User-2 Male 2024-01-29
3 User-3 Male 2024-01-29
4 User-4 Male 2024-01-29
对应的读取实现如下,可以发现与之前的读取代码完全一致。类似的,在写入时通过上述的注解属性映射的方式,可以数据文件的表头由默认的实体类属性名转为我们配置的 value
值。
@Test
public void read() {
String fileLocate = "src\\main\\resources\\data.xls";
try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
List<User> list = EasyExcel.read(in)
.head(User.class)
.sheet()
.doReadSync();
System.out.println(list);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
四、事件监听
1. 基本介绍
在 EasyExcel
中执行数据的读取写入时,其也提供了监听器用于在读写过程中的操作。
监听器的声明方式十分简单,通过继承 AnalysisEventListener<>
泛型接口即可,如下述示例中即定义了一个针对 User
类数据读取的监听器。
public class ReadListener extends AnalysisEventListener<AnnoUser> {
}
2. 事件监听
在继承上述接口后,需要重写下表中的事件方法,详细的事件描述参考下表:
方法 | 作用 |
---|---|
invokeHeadMap() | 读取 headRowNumber() 指定的表头信息。 |
invoke() | 读取动作的触发器,每读取一行数据都将触发。 |
onException() | 读取数据异常时的事件方法。 |
doAfterAllAnalysed() | 文件内所有数据读取完成后的触发事件。 |
下述为一个完成的事件监听器定义示例,代码如下:
public class ReadListener extends AnalysisEventListener<AnnoUser> {
private final List<AnnoUser> dataList = new ArrayList<>();
/**
* 文件列名表头读取,可执行内容格式校验
*
* @param headMap 文件表头
* @param context 上下文对象
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
super.invokeHeadMap(headMap, context);
}
/**
* 读取动作的触发器,可对读取的对应数据执行操作
*
* @param row 当前行数据
* @param context 上下文对象
*/
@Override
public void invoke(AnnoUser row, AnalysisContext context) {
// 可执行数据合法性校验或格式转化
dataList.add(row);
}
/**
* 读取数据异常时的触发器
*
* @param exception 异常对象
* @param context 上下文对象
*/
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
// 异常处理
}
/**
* 文件内所有数据读取完成后的触发事件
*
* @param context 上下文对象
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 对完整的数据执行操作
System.out.println("Listener after all: " + dataList);
}
}
3. 应用示例
监听器的使用方式同样十分简单,以数据读取为例,在声明 read()
属性时传入即可。
如下述为监听器的使用示例,与之前的不同仅在于在 read()
中多了 listener
参数。
@Test
public void read() {
String fileLocate = "src\\main\\resources\\data.xls";
try (InputStream in = Files.newInputStream(Paths.get(fileLocate))) {
// 创建监听器
ReadListener listener = new ReadListener();
// 传入监听器
List<User> list = EasyExcel.read(in, listener)
.head(User.class)
.sheet()
.doReadSync();
System.out.println(list);
} catch (Exception e) {
throw new RuntimeException(e);
}
}