EasyExcel工作表操作

这一部分,我们结合具体的案例介绍 EasyExcel 是如何将数据写入到一个或多个工作表中。

一、一个实体类重复多次写入到一个工作表中

首先我们来看一个简单的案例:利用循环将数据写到一个工作表中。

案例:将员工信息重复 5 次写入到一个工作表中,并将下面的工作表名称设置成“测试工作表”。

实体类 Employee、工具类 EmployeeUtils 中一次性获取十条 Employee 数据的getEmployeeList方法也已经准备好:点我查看

导出的 Excel 文件显示效果如下图所示:

以下是我的实现思路:

  1. 创建一个可以执行 5 次的循环:案例中要求我们重复 5 次写入到一个工作表中,这里我们使用 for 循环、while 循环或者 do-while 循环都可以。
  2. 在这个循环中,获取到数据列表,调用EmployeeUtils.getEmployeeList方法。
  3. 使用前面学过的链式调用,将获取到的数据列表作为参数传入。

以下是实现代码:

1
2
3
4
5
6
7
8
@Test
public void testIterateWriteWorksheet1() {
String filePath = EmployeeUtils.getFilePath();
for (int i = 0; i < 5; i++) {
EasyExcel.write(filePath, Employee.class).sheet("测试工作表").doWrite(EmployeeUtils.getDataList());
}
System.out.println("数据已写入到Excel文件中");
}

运行这段代码,我们发现它在 resources 目录下生成了 Excel 文件,但是 Excel 文件中只有 10 条数据,显然并不符合案例中的要求:

1.1 为什么链式调用没有达到效果

为什么我们的链式调用没有达到效果呢?

要想解答上述疑问,我们需要观察一下链式调用写法:

1
EasyExcel.write(filePath, Employee.class).sheet("测试工作表").doWrite(EmployeeUtils.getDataList());

只凭英文语义我们也可以知道,真正将数据写入的是doWrite方法,查看doWrite方法源码我们可以看到如下逻辑:

1
2
3
4
5
6
7
public void doWrite(Collection<?> data) {
if (excelWriter == null) {
throw new ExcelGenerateException("Must use 'EasyExcelFactory.write().sheet()' to call this method");
}
excelWriter.write(data, build());
excelWriter.finish();
}

结合我们的链式调用内容,我们传入的参数data对应的是EmployeeUtils.getDataList方法的列表数据。

源码中 2~4 行是判断excelWriter是否为空,只要我们在使用链式调用方式,这个excelWriter就不为空,即excelWriter != null。接下来我们就通过源码来验证上述内容是否正确。

我们来查看一下链式调用的sheet方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public ExcelWriterSheetBuilder sheet(Integer sheetNo, String sheetName) {
ExcelWriter excelWriter = build();
ExcelWriterSheetBuilder excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter);
if (sheetNo != null) {
excelWriterSheetBuilder.sheetNo(sheetNo);
}
if (sheetName != null) {
excelWriterSheetBuilder.sheetName(sheetName);
}
return excelWriterSheetBuilder;
}

public ExcelWriter build() {
return new ExcelWriter(writeWorkbook);
}

在调用sheet方法时,源码中首先调用了build方法,而build方法中正好使用了new的方式创建了 ExcelWriter 对象,因此上述的excelWriter != null成立。

执行了上述判断以后,在doWrite方法中使用excelWriter依次调用了writefinish方法。其中write方法的作用是将数据写入到 Excel 文件中,finish方法是关闭文件资源流(毕竟写入到 Excel 操作属于文件 IO 操作),这一轮循环执行结束。

进行下一轮循环的时候,仍然执行上述的代码逻辑,数据再次写入 Excel 文件时,会覆盖掉上一次的 Excel 数据,以此类推,当循环结束时,Excel 文件中的内容是最后一次循环写入的数据。

1.2 解决方案

在循环中直接使用链式调用并不能达到效果,那么还有其他解决方案吗?

当然有!我们在前面学过 try-with-resources 方式实现将数据写入到 Excel 文件中,这种方式的特点是在保证整个 try 块代码没有出现异常时,在整个 try 块代码执行完成后,自动关闭文件资源流,在循环过程中不涉及到数据覆盖的操作。

以下是使用 try-with-resources 方式的实现思路:

  1. 在 try 小括号中创建 ExcelWriter 类型对象excelWriter:因为 ExcelWriter 类实现了 Closeable 接口,在 try 块执行完毕时,会自动调用close方法关闭文件资源流。
  2. 在 try 块中编写可以执行 5 次的循环,在循环中设置工作表相关信息(例如:工作表的名称)。
  3. excelWriter 对象调用 write 方法,配置要写入的数据列表和工作表对象作为参数,执行数据写入操作。
  4. 如果整个 try 块执行过程中未出现异常,此时会自动关闭文件资源流。

以下是使用 try-with-resources 方式的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testIterateWriteWorksheet2() {
String filePath = EmployeeUtils.getFilePath();
// try-with-resources方式实现数据的循环写入
try (ExcelWriter excelWriter = EasyExcel.write(filePath, Employee.class).build()) {
for (int i = 0; i < 5; i++) {
WriteSheet writeSheet = EasyExcel.writerSheet(0, "测试工作表").build();
excelWriter.write(EmployeeUtils.getDataList(), writeSheet);
}
}
System.out.println("数据已写入到Excel文件中");
}

这段代码运行后,在 resources 目录下生成了一个 Excel 文件,此时 Excel 文件中除去表头,一共有 50 行数据(共循环了 5 次,每次有 10 条数据写入到 Excel 文件):

新思路和原思路相比,新思路在执行循环的过程中文件资源流一致保持开启状态,直到整个 try 块执行完毕自动关闭文件资源流;而原思路是每次执行循环时开启文件资源流,循环结束后关闭本次文件资源流。

二、一个实体类重复多次写入到多个工作表中

案例:将上述的 Employee 列表数据重复写入到 5 张工作表中,效果如下图所示

已知 Employee 实体类和 EmployeeUtils 实体类已经准备好:点我查看

前面我们提到过,一个 Excel 文件称作一个工作簿(Workbook),在一个工作簿中可以包含多张工作表(Worksheet)。我们在前面向 Excel 中写数据时,一般是一个对象写入到一个工作表中。但有的时候,我们需要向多个工作表中插入数据。

EasyExcel 在处理多个工作表时,类似数组的索引,会将第一张要操作的工作表标记为 0,第二张要操作的工作表标记为 1,以此类推(如下图):

结合上述分析,我们在调用 EasyExcel 类的静态方法writerSheet时,可以指定索引(sheetNo)和工作表名称(sheetName)。那么我们可以编写一个$[0,5)$的普通 for 循环,索引就是用 for 循环每一次迭代的数字,并为每一个工作表指定名称。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testIterateWriteSheet() {
String filePath = EmployeeUtils.getFilePath();
try (ExcelWriter excelWriter = EasyExcel.write(filePath, Employee.class).build()) {
for (int i = 0; i < 5; i++) {
WriteSheet writeSheet = EasyExcel.writerSheet(i, "测试工作表" + (i + 1)).build();
excelWriter.write(EmployeeUtils.getDataList(), writeSheet);
}
}
System.out.println("数据成功写出到Excel文件中");
}

运行这一段代码,发现生成的 Excel 文件中存在 5 个工作表,并且每一个工作表中存在表头和 10 行数据。

三、多个实体类写入到多个工作表中

案例:使用 EasyExcel 将学生数据和教师数据分别写入到一个 Excel 文件的两个工作表中,效果如下图所示:

已知学生实体类 Student、教师实体类 Teacher 代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
* 学生实体类
*
* @author iCode504
* @date 2024-05-10
*/
@Data
@NoArgsConstructor
@ToString
public class Student {

@ExcelProperty("学生id")
private String studentId;
@ExcelProperty("学生姓名")
private String studentName;
@ExcelProperty("年龄")
private Integer age;
@ExcelProperty("性别")
private String gender;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
* 教师实体类
*
* @author iCode504
* @date 2024-05-10
*/
@Data
@NoArgsConstructor
@ToString
public class Teacher {
@ExcelProperty("教师id")
private String teacherId;
@ExcelProperty("教师姓名")
private String teacherName;
@ExcelProperty("学科")
private String subject;
@ExcelProperty("年龄")
private Integer age;
}

数据处理类 PersonUtils:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
* 人员工具类
*
* @author iCode504
* @date 2024-05-10
*/
public class PersonUtils {

public static final String[] SUBJECTS = {"语文", "数学", "英语", "计算机", "物理", "化学", "生物"};

/**
* 获取学生信息列表
*
* @return 学生信息列表:学生id从1开始,姓名默认以student开头,性别随机生成
* 年龄在12-18岁之间随机生成
*/
public static List<Student> getStudentInfoList() {
List<Student> list = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 100; i++) {
Student student = new Student();
student.setStudentId(String.valueOf(i + 1));
student.setStudentName("student" + (i + 1));
student.setGender(random.nextBoolean() ? "男" : "女");
student.setAge(random.nextInt(7) + 12);
list.add(student);
}
return list;
}

/**
* 获取教师信息列表
*
* @return 教师信息列表:教师id默认从1生成,姓名默认以teacher开头,年龄在30-59岁之间随机生成
* 学科随机生成
*/
public static List<Teacher> getTeacherInfoList() {
List<Teacher> list = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 10; i++) {
Teacher teacher = new Teacher();
teacher.setTeacherId(String.valueOf(i + 1));
teacher.setTeacherName("teacher" + (i + 1));
teacher.setAge(random.nextInt(30) + 30);
teacher.setSubject(SUBJECTS[random.nextInt(SUBJECTS.length)]);
list.add(teacher);
}
return list;
}


/**
* 生成Excel文件名称及所在路径,这里为了方便,我将最终生成的Excel放到了当前项目的resources目录下,以当前时间戳命名。方便后续使用
* 如有需要,请将第66行代码替换成你自己的路径
*/
public static String getFilePath() {
return "E:\\Code\\icode504-codespace\\easyexcel-study\\easyexcel-demo1\\src\\main\\resources\\"
+ System.currentTimeMillis() + ".xlsx";
}
}

由于我们需要将两个不相同的实体列表(学生信息列表PersonUtils.getStudentInfoList和教师信息列表PersonUtils.getTeacherInfoList)写入到两张数据表中,无法使用循环实现上述过程。以下是解决上述案例的思路:

  1. 使用try-with-resources方式创建。由于 ExcelWriter 类实现了 Closeable 接口,代码执行完成后会自动调用close方法自动关闭资源;
  2. try小括号中创建 ExcelWriter 类型对象excelWriter:调用EasyExcel.write方法时,只需要添加文件路径参数,不需要添加类的字节码参数(因为要写入的数据是两个实体类,write方法默认只能接收一个类字节码参数)。参数配置完成后,调用build方法创建excelWriter对象;
  3. 调用两次EasyExcel.writeSheet方法,第一个参数指定要写入哪一个数据表中(EasyExcel 处理数据表默认从 0 开始),第二个参数可以指定工作表的名称(后续更容易分辨)。参数配置完成后,调用build方法创建writeSheet对象;
  4. excelWriter对象调用write方法分别将学生和教师的数据列表分别写入 Excel 文件的两个数据表中。

以下是代码是上述思路的实现:

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testWorksheet3() {
try (ExcelWriter excelWriter = EasyExcel.write(PersonUtils.getFilePath()).build()) {
// 分两次调用writerSheet和write方法,分别将学生信息和教师信息写入到两张数据表中
WriteSheet studentWriteSheet = EasyExcel.writerSheet(0, "学生信息表").build();
WriteSheet teacherWriteSheet = EasyExcel.writerSheet(1, "教师信息表").build();
excelWriter.write(PersonUtils.getStudentInfoList(), studentWriteSheet);
excelWriter.write(PersonUtils.getTeacherInfoList(), teacherWriteSheet);
}
System.out.println("数据已写入到Excel文件中");
}

运行这段代码,生成的 Excel 文件确实有两张表:学生信息表、教师信息表,两张表里面确实有数据。但是有一点点小瑕疵:没有表头。

解决这个问题也很简单,只需要在writeSheet方法后面再调用head方法,并在这个方法中指定类字节码参数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testWorksheet3() {
try (ExcelWriter excelWriter = EasyExcel.write(PersonUtils.getFilePath()).build()) {
// 分两次调用writerSheet和write方法,分别将学生信息和教师信息写入到两张数据表中
// 如果需要将表头信息写入到工作表中,就在writerSheet方法后面再调用head方法并传入类的字节码信息
WriteSheet studentWriteSheet = EasyExcel.writerSheet(0, "学生信息表").head(Student.class).build();
WriteSheet teacherWriteSheet = EasyExcel.writerSheet(1, "教师信息表").head(Teacher.class).build();
excelWriter.write(PersonUtils.getStudentInfoList(), studentWriteSheet);
excelWriter.write(PersonUtils.getTeacherInfoList(), teacherWriteSheet);
}
System.out.println("数据已写入到Excel文件中");
}

此时两张工作表中就都存在表头信息了:

四、知识点总结

Easyexcel 工作表操作知识点总结如下图所示: