在 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。 |
下述通过代码示例读取刚才准备的 Excel
文件,内容如下:
public class ExcelReadTest {
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);
}
}
}
需注意的是在使用 head(xxx.class)
时默认数据字段映射关系是与类属性定义顺序一致的。
以刚才创建的 User
类为例,Excel
文件中列的定义顺序为 id, name, gender, birthday
,则在赋值时其默认实体类中第一个属性即为 id
并以此类推。如果此时 User
的属性定义顺序为 name, id
那么最终读取的数据 id
值将会被赋值给 name
字段。
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
可修改表头顺序。
属性 | 描述 |
---|---|
value | 定义表头,输入多个则合并单元格。 |
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;
}
四、读取监听
1. 基本介绍
在 EasyExcel
中执行数据的读取写入时,其也提供了监听器用于在读写过程中的操作。
监听器的声明方式十分简单,通过继承 AnalysisEventListener<>
接口即可,其针对数据的读取提供了一系统回调函数,由此我们便可实现更灵活的数据操作。
public class ReadListener<T> extends AnalysisEventListener<T> {
}
2. 事件监听
在继承上述接口后,需要重写下表中的事件方法,详细的事件描述参考下表:
方法 | 作用 |
---|---|
invokeHeadMap() | 读取 headRowNumber() 指定的表头信息。 |
invoke() | 读取动作的触发器,每读取一行数据都将触发。 |
onException() | 读取数据异常时的事件方法。 |
doAfterAllAnalysed() | 文件内所有数据读取完成后的触发事件。 |
下述为一个完成的事件监听器定义示例,代码如下:
@Getter
public class ReadListener<T> extends AnalysisEventListener<T> {
private final Class<T> tClass;
private final List<T> dataList = new ArrayList<>();
public ReadListener(Class<T> tClass){
this.tClass = tClass;
}
/**
* 文件列名表头读取,可执行内容格式校验
*
* @param headMap 文件表头
* @param context 上下文对象
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
}
/**
* 读取行数据的触发器,可执行数据校验
*
* @param row 当前行数据
* @param context 上下文对象
*/
@Override
public void invoke(T 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<User> listener = new ReadListener<>(User.class);
// 传入监听器并操作读取
EasyExcel.read(in, listener)
.head(User.class)
.sheet()
.doReadSync();
// 读取结果
System.out.println(listener.getDataList());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
五、写入监听
1. 内容样式
在文件数据写入时,与读取监听器类似,EasyExcel
提供了对应 CellWriteHandler
可实现数据写入时的回调。
其集成方式并不复杂,通过实现 CellWriteHandler
接口重写 afterCellDispose()
,在写入每个单元格数据后将会执行此回调事件。
如下述示例即通过 CellWriteHandler
实现表头每列首字符标红。
public class RedCellWriteHandler implements CellWriteHandler {
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
Integer rowIndex = context.getRowIndex();
Integer columnIndex = context.getColumnIndex();
if (rowIndex < 0 || columnIndex > 0) {
return;
}
Cell cell = context.getCell();
String content = cell.getStringCellValue();
if (StringUtils.isBlank(content)) {
// 空值不处理
return;
}
Workbook workbook = cell.getSheet().getWorkbook();
// 设置字体
Font font = workbook.createFont();
font.setBold(true);
font.setColor(IndexedColors.RED.getIndex());
RichTextString richText = workbook.getCreationHelper().createRichTextString(content);
richText.applyFont(0, 1, font);
// 回写单元格
cell.setCellValue(richText);
}
}
2. 代码集成
针对声明的处理器,在写入时通过 registerWriteHandler()
方法注册。
仍以前文中的写入为例,其对应的集成代码如下,注意需添加 inMemory(true)
使之生效。
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)
.inMemory(true)
// 注册处理器
.registerWriteHandler(new RedCellWriteHandler())
.head(User.class)
.doWrite(data);
}