Apache poi 和 EasyExcel

该笔记学习b站视频制作而成:POIEasyExcel

一. 简介

Java解析、生成Excel比较有名的框架有Apache poi、jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大。
easyexcel重写了poi对07版Excel的解析,一个3M的excel用POI sax解析依然需要100M左右内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出;03版依赖POI的sax模式,在上层做了模型转换的封装,让使用者更加简单方便

EasyExcel能大大减少内存的原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一行一行读取数据,逐个解析


下图是EasyExcel和POl在解析Excel时的对比图

image-20240316133120557

二. Apache poi

1. Excel基本写操作

创建项目并导入依赖

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
<dependencies>
<!--xls(03版本)-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.9</version>
</dependency>
<!--xlsx(07版本)-->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.9</version>
</dependency>
<!--日期格式化工具-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.1</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>

Excel的结构

image-20240318134425789

Excel有03版(xls)和07版(xlsx),03版的一个表最多有65536行


在idea中创建Excel表会有03版和07版两个版本

创建工作簿时使用WorkBook接口的实现类

image-20240318135330304

07版的操作方法和03版的一模一样,只需要在创建WorkBook对象时使用07版的实现类即可

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
65
66
67
68
69
70
71
72
73
74
75
76
77
public class ExcelWrite {

String PATH = "E:\\workspace\\esayexcel";

// 03版
@Test
public void testWrite03() throws Exception{
// 1.创建一个工作簿
Workbook workbook = new HSSFWorkbook();
// 2.创建一个工作表
Sheet sheet = workbook.createSheet("216寝室登记表");
// 3.创建一个行 第一行
Row row1 = sheet.createRow(0); // 从0开始
// 4.创建一个第一个 第一行第一个(0,0)
Cell cell11 = row1.createCell(0);
// 添加信息
cell11.setCellValue("学生姓名");

// (1,2)
Cell cell12 = row1.createCell(1);
cell12.setCellValue("出生日期");

// (2.1)
Row row2 = sheet.createRow(1);
Cell cell21 = row2.createCell(0);
cell21.setCellValue("张三");

// (2,2)
Cell cell22 = row2.createCell(1);
cell22.setCellValue("2000-1-1");

// 通过IO流生成表, 03版的使用xls结尾
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "寝室登记表03.xls");
workbook.write(fileOutputStream);
// 关闭流
fileOutputStream.close();
System.out.println("寝室表生成成功");

}


// 07版
@Test
public void testWrite07() throws Exception{
// 1.创建一个工作簿
Workbook workbook = new XSSFWorkbook();
// 2.创建一个工作表
Sheet sheet = workbook.createSheet("217寝室登记表");
// 3.创建一个行 第一行
Row row1 = sheet.createRow(0); // 从0开始
// 4.创建一个第一个 第一行第一个(0,0)
Cell cell11 = row1.createCell(0);
// 添加信息
cell11.setCellValue("学生姓名");

// (1,2)
Cell cell12 = row1.createCell(1);
cell12.setCellValue("出生日期");

// (2.1)
Row row2 = sheet.createRow(1);
Cell cell21 = row2.createCell(0);
cell21.setCellValue("李四");

// (2,2)
Cell cell22 = row2.createCell(1);
cell22.setCellValue("2000-2-2");

// 通过IO流生成表, 03版的使用xls结尾
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "寝室登记表07.xlsx");
workbook.write(fileOutputStream);
// 关闭流
fileOutputStream.close();
System.out.println("寝室表生成成功");

}
}

注意对象的区别和文件名后缀

运行后会获得03版和07版的两个Excel表

image-20240318141255081

image-20240318141333514 image-20240318141400067

2. 大数据量的写入

  • 大文件写HSSF(03版)

    • 缺点:最多只能处理65536行,否则会抛出异常

      1
      java.lang.IllegalArgumentException: Invalid row number (65536) outside allowable range (0..65536)
    • 优点:过程中写入缓存,不操作磁盘,最后一次性写入磁盘,速度快

  • 大文件写XSSF(07版)

    • 缺点:写数据时速度非常慢,非常消耗内存,也会发生内存溢出,如100万条
    • 优点:可以写较大的数据量,如20万条
  • 大文件写SXSSF(07版)

    • 优点:可以写非常大的数据量,如100万条甚至更多条,写数据速度快,占用更少的内存
    • 注意:过程会产生临时文件,需要清理临时文件。默认由100条记录被保存在内存中,如果超过这个数量,则最前面的数据被写入临时文件。如果想自定义内存中的数据的数量,可以使用new SXSSFWorkbook(数量)

大数据量写HSSF代码实现

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
public class ExcelWriteTest {

String PATH = "E:\\workspace\\esayexcel\\esayexcel";

@Test
public void testWriteBigData03() throws Exception {
// 开始时间
long beginTime = System.currentTimeMillis();

// 创建一个工作簿
Workbook workBook = new HSSFWorkbook();
// 创建表
Sheet sheet = workBook.createSheet();
// 写入数据
for (int rowNum = 0; rowNum < 65536; rowNum++) {
// 创建行
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 10; cellNum++) {
// 创建单元格
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum); // 输入数据
}
}
// 使用IO流
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWriteBigData03.xls");
workBook.write(fileOutputStream);
fileOutputStream.close(); // 关闭流

// 结束时间
long endTime = System.currentTimeMillis();

System.out.println("over");
System.out.println((double) (endTime-beginTime)/1000);
}

}
image-20240318161147364 image-20240318161223172

超过65536行数据会报错

image-20240318161344015

大数据量写XSSF代码实现

这里写入的行数是65537行,超过了65536

虽然XSSF可以写入更多的数据,但是速度明显变慢

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
public class ExcelWriteTest {

String PATH = "E:\\workspace\\esayexcel\\esayexcel";

@Test
public void testWriteBigData07() throws Exception {
// 开始时间
long beginTime = System.currentTimeMillis();

// 创建一个工作簿
Workbook workBook = new XSSFWorkbook();
// 创建表
Sheet sheet = workBook.createSheet();
// 写入数据
for (int rowNum = 0; rowNum < 65537; rowNum++) {
// 创建行
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 10; cellNum++) {
// 创建单元格
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum); // 输入数据
}
}
// 使用IO流
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWriteBigData07.xlsx");
workBook.write(fileOutputStream);
fileOutputStream.close(); // 关闭流

// 结束时间
long endTime = System.currentTimeMillis();

System.out.println("over");
System.out.println((double) (endTime-beginTime)/1000);
}
}

image-20240318161820172 image-20240318161857515

大数据量写SXSSF的代码实现

这里相比于上面两种,多了一个清除临时文件的操作

((SXSSFWorkbook) workBook).dispose(); // 清除临时文件

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
public class ExcelWriteTest {

String PATH = "E:\\workspace\\esayexcel\\esayexcel";

@Test
public void testWriteBigDataS07() throws Exception {
// 开始时间
long beginTime = System.currentTimeMillis();

// 创建一个工作簿
Workbook workBook = new SXSSFWorkbook();
// 创建表
Sheet sheet = workBook.createSheet();
// 写入数据
for (int rowNum = 0; rowNum < 100000; rowNum++) {
// 创建行
Row row = sheet.createRow(rowNum);
for (int cellNum = 0; cellNum < 10; cellNum++) {
// 创建单元格
Cell cell = row.createCell(cellNum);
cell.setCellValue(cellNum); // 输入数据
}
}
// 使用IO流
FileOutputStream fileOutputStream = new FileOutputStream(PATH + "testWriteBigDataS07.xlsx");
workBook.write(fileOutputStream);
fileOutputStream.close(); // 关闭流

((SXSSFWorkbook) workBook).dispose(); // 清除临时文件

// 结束时间
long endTime = System.currentTimeMillis();

System.out.println("over");
System.out.println((double) (endTime-beginTime)/1000);
}
}


可以看见写入10万条数据只用了1.666s,速度提升非常大

image-20240318163006705

image-20240318163108261

3. Excel基本读取

在使用读取单元格的值时,不同的数据类型对应着不同的值

HSSF读的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ExcelReadTest {
String PATH = "E:\\workspace\\esayexcel\\";

@Test
public void testRead03() throws Exception {
// 创建一个输入流
FileInputStream inputStream = new FileInputStream(PATH + "寝室登记表03.xls");

// 1. 创建工作簿,使用Excel能操作的它都可以操作
Workbook workbook = new HSSFWorkbook(inputStream);
// 2. 获取工作表,第一个表
Sheet sheet = workbook.getSheetAt(0);
// 3. 获取行,第一行
Row row = sheet.getRow(0);
// 4. 获取单元格,第一个单元格
Cell cell = row.getCell(0);
// 获取字符串类型的值
System.out.print("获取寝室登记表03.xls的第一行第一个单元格的值:");
System.out.println(cell.getStringCellValue());
// 关闭流
inputStream.close();
}
}
image-20240318170840559

HSSF读的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ExcelReadTest {
String PATH = "E:\\workspace\\esayexcel\\";

@Test
public void testRead07() throws Exception {
// 创建一个输入流
FileInputStream inputStream = new FileInputStream(PATH + "寝室登记表07.xlsx");

// 1. 创建工作簿,使用Excel能操作的它都可以操作
Workbook workbook = new XSSFWorkbook(inputStream);
// 2. 获取工作表,第一个表
Sheet sheet = workbook.getSheetAt(0);
// 3. 获取行,第二行
Row row = sheet.getRow(1);
// 4. 获取单元格,第一个单元格
Cell cell = row.getCell(0);
// 获取字符串类型的值
System.out.print("获取寝室登记表07.xlsx的第二行第一个单元格的值:");
System.out.println(cell.getStringCellValue());
// 关闭流
inputStream.close();
}

}
image-20240318171234901

4. 读取不同类型的数据

测试表名:明细表.xls

数据:

image-20240318200432443


代码实现

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
65
66
67
68
69
70
71
72
73
74
75
76
77
public class ExcelReadTest {
String PATH = "E:\\workspace\\esayexcel\\";


@Test
public void testCellType() throws Exception {
// 获取文件流
FileInputStream inputStream = new FileInputStream(PATH + "明细表.xls");
// 创建一个工作簿
Workbook workbook = new HSSFWorkbook(inputStream);
// 获取工作表
Sheet sheet = workbook.getSheetAt(0);
// 获取标题内容,也就是表的第一行
Row rowTitle = sheet.getRow(0);
if (rowTitle != null) {
// 获取行的单元格个数
int cellCount = rowTitle.getPhysicalNumberOfCells();
for (int cellNum = 0; cellNum < cellCount; cellNum++) {
// 获取每一个单元格并输出
Cell cell = rowTitle.getCell(cellNum);
if (cell != null) {
int cellType = cell.getCellType();
String cellValue = cell.getStringCellValue();
System.out.print(cellValue + " | ");
}
}
System.out.println();
}

// 获取表中数据
// 获取有多少行
int rowCount = sheet.getPhysicalNumberOfRows();
for (int rowNum = 1; rowNum < rowCount; rowNum++) {
Row row = sheet.getRow(rowNum);
// 读取单元格
int cellCount = row.getPhysicalNumberOfCells();
for (int cellNum = 0; cellNum < cellCount; cellNum++) {
Cell cell = row.getCell(cellNum);
System.out.print("[" + (rowNum + 1) + "-" + (cellNum + 1) + "]");
// 匹配列的数据类型
if (cell != null) {
int cellType = cell.getCellType();
String cellValue = "";
switch (cellType) {
case Cell.CELL_TYPE_STRING: // 字符串
System.out.print("【String】");
cellValue = cell.getStringCellValue();
break;
case Cell.CELL_TYPE_BOOLEAN: // 布尔
System.out.print("【Boolean】");
cellValue = String.valueOf(cell.getBooleanCellValue());
break;
case Cell.CELL_TYPE_BLANK: // 空
System.out.print("【Blank】");
break;
case Cell.CELL_TYPE_NUMERIC: // 数组(日期或普通数字)
System.out.print("【numeric】");
if (HSSFDateUtil.isCellDateFormatted(cell)) { // 如果是日期
System.out.print("【日期】");
Date date = cell.getDateCellValue();
cellValue = new DateTime(date).toString("yyyy-MM-dd");
} else { // 普通数字,转成字符串输出,防止数字过长
System.out.print("【转成字符串输出】");
cell.setCellType(Cell.CELL_TYPE_STRING);
cellValue = cell.toString();
}
break;
case Cell.CELL_TYPE_ERROR: // 错误
System.out.print("【数据类型错误】");
break;
}
System.out.println(cellValue);
}
}
}
}
}

测试结果

测试结果只截取了一部分

image-20240318200636852

三. EasyExcel

官方文档

导入依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.3</version>
</dependency>

1. 写操作

1.1 简单写

方式一

创建一个模板类User

@ExcelProperty:在表格中的名字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
public class User {
@ExcelProperty("用户编号")
private Integer userId;
@ExcelProperty("姓名")
private String name;
@ExcelProperty("性别")
private String gender;
@ExcelProperty("工资")
private Double salary;
@ExcelProperty("入职时间")
private Date hireDate;

public User(Integer userId, String name, String gender, Double salary, Date hireDate) {
this.userId = userId;
this.name = name;
this.gender = gender;
this.salary = salary;
this.hireDate = hireDate;
}
}

测试类

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
public class WriteTest {
public String PATH = "E:\\workspace\\esayexcel\\";

/**
* @description: 简单写方式一
*/
@Test
public void test01() {
// 创建文件名
String fileName = PATH + "user1.xlsx";
// 根据User模板构建数据
List<User> users = new ArrayList<>();
User user1 = new User(1001,"张三","男",1000.12,new Date());
User user2 = new User(1002,"李四","男",2000.41,new Date());
User user3 = new User(1003,"王五","女",1402.36,new Date());
User user4 = new User(1004,"赵六","男",3000.11,new Date());
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 写入文件中
EasyExcel.write(fileName, User.class).sheet("用户信息表").doWrite(users);
System.out.println("写入成功");
}


/**
* @description: 简单写方式二
把 EasyExcel.write(fileName, User.class).sheet("用户信息表").doWrite(users)的每一步都拆分开来
需要手动关闭
*/
@Test
public void test02() {
// 创建文件名
String fileName = PATH + "user2.xlsx";
// 根据User模板构建数据
List<User> users = new ArrayList<>();
User user1 = new User(1001,"Java","男",1000.12,new Date());
User user2 = new User(1002,"C","男",2000.41,new Date());
User user3 = new User(1003,"CPP","男",1402.36,new Date());
User user4 = new User(1004,"Python","男",3000.11,new Date());
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 写入文件中
// 创建ExcelWriter对象
ExcelWriter excelWriter = EasyExcel.write(fileName, User.class).build();
// 创建writerSheet对象
WriteSheet writeSheet = EasyExcel.writerSheet("用户信息表").build();
excelWriter.write(users, writeSheet);
// 关闭
excelWriter.finish();
System.out.println("写入成功");
}
}

方式一

image-20240320111256698 image-20240320111325724

方式二

image-20240320113629889

1.2 排除或指定写入字段

排除字段

如果有一些类的属性,我们不想写入表中,可以通过excludeColumnFiledNames()排除掉

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
/**
* @description: 写的时候排除某些字段
*/
@Test
public void test03() {
// 创建文件名
String fileName = PATH + "user3.xlsx";
// 根据User模板构建数据
List<User> users = new ArrayList<>();
User user1 = new User(1001,"Java","男",1000.12,new Date());
User user2 = new User(1002,"C","男",2000.41,new Date());
User user3 = new User(1003,"CPP","男",1402.36,new Date());
User user4 = new User(1004,"Python","男",3000.11,new Date());
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 要排除的字段
Set<String> set = new HashSet<>();
set.add("hireDate");
set.add("salary");
set.add("gender");
// 写入文件中
EasyExcel.write(fileName, User.class).excludeColumnFiledNames(set).sheet("用户信息表").doWrite(users);
System.out.println("写入成功");
}

image-20240320114606079

指定字段

指定要写入表格中的属性可以通过includeColumnFiledNames()来指定

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
/**
* @description: 写的时候指定某些字段
*/
@Test
public void test04() {
// 创建文件名
String fileName = PATH + "user4.xlsx";
// 根据User模板构建数据
List<User> users = new ArrayList<>();
User user1 = new User(1001,"Java","男",1000.12,new Date());
User user2 = new User(1002,"C","男",2000.41,new Date());
User user3 = new User(1003,"CPP","男",1402.36,new Date());
User user4 = new User(1004,"Python","男",3000.11,new Date());
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 要排除的字段
Set<String> set = new HashSet<>();
set.add("hireDate");
set.add("salary");
set.add("gender");
// 写入文件中
EasyExcel.write(fileName, User.class).includeColumnFiledNames(set).sheet("用户信息表").doWrite(users);
System.out.println("写入成功");
}

image-20240320115134530

1.3 对excel中的字段排序

写入时如果要想对excel中的字段排序,可以在类属性上的注解@ExcelProperty添加index属性来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Data
public class User {
@ExcelProperty(value = "用户编号", index = 0)
private Integer userId;
@ExcelProperty(value = "姓名", index = 1)
private String name;
@ExcelProperty(value = "性别", index = 2)
private String gender;
@ExcelProperty(value = "工资", index = 4)
private Double salary;
@ExcelProperty(value = "入职时间", index = 3)
private Date hireDate;

public User(Integer userId, String name, String gender, Double salary, Date hireDate) {
this.userId = userId;
this.name = name;
this.gender = gender;
this.salary = salary;
this.hireDate = hireDate;
}
}


1.4 复杂头的数据写入

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class ComplexHeadUser {
@ExcelProperty({"主实现","用户id"})
private Integer userId;
@ExcelProperty({"用户信息","用户姓名"})
private String name;
@ExcelProperty({"用户信息","用户性别"})
private String gender;

public ComplexHeadUser(Integer userId, String name, String gender) {
this.userId = userId;
this.name = name;
this.gender = gender;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @description: 复杂头数据写入
*/
@Test
public void test05() {
// 创建文件名
String fileName = PATH + "user5.xlsx";
// 根据User模板构建数据
List<ComplexHeadUser> users = new ArrayList<>();
ComplexHeadUser user1 = new ComplexHeadUser(1001,"Java","男");
ComplexHeadUser user2 = new ComplexHeadUser(1002,"C","男");
users.add(user1);
users.add(user2);

// 写入文件中
EasyExcel.write(fileName, ComplexHeadUser.class).sheet("用户信息表").doWrite(users);
System.out.println("写入成功");
}

生成的表

image-20240320135731838

1.5 重复多次写入

重复写入到一个Sheet中

只需要在excelWriter.write时传入同一个Sheet即可

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
/**
* @description: 重复写入同一个sheet中
*/
@Test
public void test06() {
// 创建文件名
String fileName = PATH + "user6.xlsx";
// 根据User模板构建数据
List<User> users = new ArrayList<>();
User user1 = new User(1001,"Java","男",1000.12,new Date());
User user2 = new User(1002,"C","男",2000.41,new Date());
User user3 = new User(1003,"CPP","男",1402.36,new Date());
User user4 = new User(1004,"Python","男",3000.11,new Date());
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 写入文件中
// 创建ExcelWriter对象
ExcelWriter excelWriter = EasyExcel.write(fileName, User.class).build();
// 创建writerSheet对象
WriteSheet writeSheet = EasyExcel.writerSheet("用户信息表").build();
// 重复写入
for (int i = 0; i < 4; i++) {
excelWriter.write(users, writeSheet);
}
// 关闭
excelWriter.finish();
System.out.println("写入成功");
}

t

image-20240320141017568

重复多次写在不同的sheet中

只需要在excelWriter.write时传入不同的Sheet即可

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
/**
* @description: 重复写入不同的sheet中
*/
@Test
public void test07() {
// 创建文件名
String fileName = PATH + "user7.xlsx";
// 根据User模板构建数据
List<User> users = new ArrayList<>();
User user1 = new User(1001,"Java","男",1000.12,new Date());
User user2 = new User(1002,"C","男",2000.41,new Date());
User user3 = new User(1003,"CPP","男",1402.36,new Date());
User user4 = new User(1004,"Python","男",3000.11,new Date());
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 写入文件中
// 创建ExcelWriter对象
ExcelWriter excelWriter = EasyExcel.write(fileName, User.class).build();
// 重复写入
for (int i = 0; i < 4; i++) {
// 创建writerSheet对象
WriteSheet writeSheet = EasyExcel.writerSheet("用户信息表" + i).build();
excelWriter.write(users, writeSheet);
}
// 关闭
excelWriter.finish();
System.out.println("写入成功");
}

image-20240320141317981

1.6 日期数字格式化

对于日期和数字有时候我们需要对其展示的样式进行格式化,easyexcel提供了一下注解

  • @DateTimeFormat : 日期格式化
  • @NumberFormat : 数字格式化(小数或者百分数)

给实体类相应的字段添加上注解

image-20240323171730432

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @description: 日期数字格式化
*/
@Test
public void test08() {
// 创建文件名
String fileName = PATH + "user8.xlsx";
// 根据User模板构建数据
List<User> users = new ArrayList<>();
User user1 = new User(1001,"张三","男",1000.121,new Date());
User user2 = new User(1002,"李四","男",2000.411,new Date());
User user3 = new User(1003,"王五","女",1402.3601,new Date());
User user4 = new User(1004,"赵六","男",3000.1,new Date());
users.add(user1);
users.add(user2);
users.add(user3);
users.add(user4);
// 写入文件中
EasyExcel.write(fileName, User.class).sheet("用户信息表").doWrite(users);
System.out.println("写入成功");
}

测试结果

image-20240323171844280

1.7 写入图片到excel

EasyExcel写入图片到excel时,支持5种数据类型的图片类型

  • 图片可以是一个File格式
  • 图片可以是一个InputStream输入流的方式
  • 图片可以是一个byte[]数组的方式
  • 图片可以是 一个网络的java.net.URL对象方式
  • 图片也可以是一个String类型方式(当是String类型是,需要StringImageConverter类型转换器)

构建包含这五种数据类型的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@ContentRowHeight(100) // 内容高度
@ColumnWidth(100 / 8) // 列宽
public class ImageData {
// 使用抽象文件表示一个图片
private File file;
// 使用输入流保存一个图片
private InputStream inputStream;
/**
* 当使用String类型保存一个图片的时候需要使用StringImageConverter转换器
*/
@ExcelProperty(converter = StringImageConverter.class)
private String string;
// 使用二进制数据保存为一个图片
private byte[] byteArray;
// 使用网络链接保存一个图片
private URL url;
}

测试方法

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
/**
* @description: 写入图片到excel
* EasyExcel提供了5种数据格式进行图片的写入
*/
@Test
public void test09() throws IOException {
// 创建文件保存的位置,以及文件名
String fileName = PATH + "user10.xlsx";

List<ImageData> list = new ArrayList<>();
ImageData imageData = new ImageData();

// 第一种:根据File写入
imageData.setFile(new File("E:\\workspace\\esayexcel\\img.png"));

// 第二种:根据InputStream写入
imageData.setInputStream(new FileInputStream("E:\\workspace\\esayexcel\\img.png"));

// 第三种:根据String类型写入
imageData.setString("E:\\workspace\\esayexcel\\img.png");

// 第四种:使用二进制方式写入,先获取文件大小,再读成二进制数组
File file = new File("E:\\workspace\\esayexcel\\img.png");
// 创建一个指定大小的二进制数组
byte[] bytes = new byte[(int)file.length()];
FileInputStream fileInputStream = new FileInputStream(file);
// 将文件流写入到二进制数组中
fileInputStream.read(bytes, 0, (int)file.length());
imageData.setByteArray(bytes);

// 第五种:只使用网络
imageData.setUrl(new URL("https://ts1.cn.mm.bing.net/th/id/R-C.5c53fb9d074c6e35302bca533c0204e5?rik=AYOvGEn5S6VPcw&riu=http%3a%2f%2fimg.pconline.com.cn%2fimages%2fupload%2fupc%2ftx%2fwallpaper%2f1301%2f10%2fc1%2f17280208_1357799853569.jpg&ehk=ckgBYNfHbwaEcgpJjb%2f7E%2bfYjy74F8b81gJaf5cBpic%3d&risl=&pid=ImgRaw&r=0"));

// 添加到集合中,然后使用EasyExcel写入
list.add(imageData);
EasyExcel.write(fileName, ImageData.class).sheet("图片").doWrite(list);
// 关闭输入流
fileInputStream.close();

}

测试结果

image-20240323180007002

1.8 设置excel表格的列宽行高

可以通过以下注解来设置excel表的列宽和行高

  • @ContentRowHeight 设置内容高度
  • @HeadRowHeight 设置标题高度
  • @ColumnWidth 设置列宽
    • @ColumnWidth如果写在类上,就是设置整个表格所有的列宽,如果写在属性上,就是设置这个属性对应的表格字段的列宽

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@ContentRowHeight(20)// 设置内容高度
@HeadRowHeight(20) // 设置标题高度
@ColumnWidth(25) // 设置列宽
public class WidthAndHeightData {
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;

// 设置宽度为50
@ColumnWidth(50)
@ExcelProperty("数字标题")
private Double doubleData;
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @description: 设置excel的列宽行高
*/
@Test
public void test10() throws IOException {
// 创建文件保存的位置,以及文件名
String fileName = PATH + "user10.xlsx";

List<WidthAndHeightData> list = new ArrayList<>();

WidthAndHeightData widthAndHeightData = new WidthAndHeightData();
widthAndHeightData.setString("正哥");
widthAndHeightData.setDate(new Date());
widthAndHeightData.setDoubleData(10000D);
list.add(widthAndHeightData);

EasyExcel.write(fileName, WidthAndHeightData.class).sheet("列宽行高").doWrite(list);
}

测试结果

image-20240324140530445

1.9 通过注解设置excel的样式
  • @HeadStyle:设置头的背景
    • 这个字段写在类上就是对所有的字段头都设置,也就是表的第一行。如果写在属性上就是只对这个属性的头设置背景颜色,属性的设置会覆盖掉类的设置
  • @HeadFontStyle:设置头字体,规则同事
  • @ContentStyle:设置内容的背景,规则同上
  • @ContentFontStyle:设置内容的字体,规则同上

实体类

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

@Data
// 头背景设置成红色 IndexedColors.RED.getIndex()
@HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 10)
// 头字体设置成20
@HeadFontStyle(fontHeightInPoints = 20)
// 内容的背景设置成绿色 IndexedColors.GREEN.getIndex()
@ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 17)
// 内容字体设置成20
@ContentFontStyle(fontHeightInPoints = 20)
public class DemoStyleData {

// 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex()
@HeadStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 14)
// 字符串的头字体设置成30
@HeadFontStyle(fontHeightInPoints = 30)
// 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex()
@ContentStyle(fillPatternType = FillPatternType.SOLID_FOREGROUND, fillForegroundColor = 40)
// 字符串的内容字体设置成30
@ContentFontStyle(fontHeightInPoints = 30)
@ExcelProperty("字符串标题")
private String string;

@ExcelProperty("日期标题")
private Date date;

@ExcelProperty("数字标题")
private Double doubleData;
}

测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @description: 通过注解设置excel表格样式
*/
@Test
public void test11() throws IOException {
// 创建文件保存的位置,以及文件名
String fileName = PATH + "user11.xlsx";

List<DemoStyleData> list = new ArrayList<>();

DemoStyleData demoStyleData = new DemoStyleData();
demoStyleData.setString("正哥");
demoStyleData.setDate(new Date());
demoStyleData.setDoubleData(10000D);
list.add(demoStyleData);

EasyExcel.write(fileName, DemoStyleData.class).sheet("excel样式").doWrite(list);
}

测试结果

image-20240324141952658

1.10 合并单元格

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
// 将第6-7行的2-3列合并成一个单元格
@OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
// 这一列 每隔2行 合并单元格
@ContentLoopMerge(eachRow = 2)
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
}

测试方法

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
/**
* @description: 合并单元格
*/
@Test
public void test12() throws IOException {
// 创建文件保存的位置,以及文件名
String fileName = PATH + "user12.xlsx";

List<DemoMergeData> list = new ArrayList<>();
DemoMergeData dmd = new DemoMergeData();
dmd.setDate(new Date());
dmd.setDoubleData(200.12);
dmd.setString("正哥");
DemoMergeData dmd2 = new DemoMergeData();
dmd2.setDate(new Date());
dmd2.setDoubleData(200.12);
dmd2.setString("正哥");
DemoMergeData dmd3 = new DemoMergeData();
dmd3.setDate(new Date());
dmd3.setDoubleData(200.12);
dmd3.setString("小王");
DemoMergeData dmd4 = new DemoMergeData();
dmd4.setDate(new Date());
dmd4.setDoubleData(200.12);
dmd4.setString("小王");
DemoMergeData dmd5 = new DemoMergeData();
dmd5.setDate(new Date());
dmd5.setDoubleData(200.12);
dmd5.setString("小李");
DemoMergeData dmd6= new DemoMergeData();
dmd6.setDate(new Date());
dmd6.setDoubleData(200.12);
dmd6.setString("小李");
DemoMergeData dmd7 = new DemoMergeData();
dmd7.setDate(new Date());
dmd7.setDoubleData(200.12);
dmd7.setString("小红");
DemoMergeData dmd8 = new DemoMergeData();
dmd8.setDate(new Date());
dmd8.setDoubleData(200.12);
dmd8.setString("小红");
list.add(dmd);
list.add(dmd2);
list.add(dmd3);
list.add(dmd4);
list.add(dmd5);
list.add(dmd6);
list.add(dmd7);
list.add(dmd8);

EasyExcel.write(fileName, DemoMergeData.class).sheet("合并单元格").doWrite(list);
}

测试结果

image-20240324143852001



2. 读操作

2.1 简单读

先创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class DemoData {
private String name;
private Date hireDate;
private Double salary;

public DemoData() {
}

public DemoData(String name, Date hireDate, Double salary) {
this.name = name;
this.hireDate = hireDate;
this.salary = salary;
}
}

测试类

方式二是在方式一的基础上进行拆分

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
/**
* @description: 最简单写,方式一
*/
@Test
public void test01() {
String fileName = PATH + "user11.xlsx";

EasyExcel.read(fileName, DemoData.class, new AnalysisEventListener<DemoData>() {
/**
* @description: 每解析一行Excel表数据,就会调用一次
*/
@Override
public void invoke(DemoData data, AnalysisContext context) {
//在这里操作,将解析的每一条数据保存到数据库中,在这里可以调用数据库
System.out.println("解析的数据为: "+data);
}

/**
* @description: 解析完被调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("解析完成");
}
}).sheet().doRead();
}



/**
* @description: 最简单写,方式二
*/
@Test
public void test02() {
String fileName = PATH + "user11.xlsx";

ExcelReader excelReader = EasyExcel.read(fileName, DemoData.class, new AnalysisEventListener<DemoData>() {
/**
* @description: 每解析一行Excel表数据,就会调用一次
*/
@Override
public void invoke(DemoData data, AnalysisContext context) {
//在这里操作,将解析的每一条数据保存到数据库中,在这里可以调用数据库
System.out.println("解析的数据为: " + data);
}

/**
* @description: 解析完被调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("解析完成");
}
}).build();
// 创建一个Sheet对象,并读取Excel的第一个sheet,下标从0开始
ReadSheet readSheet = EasyExcel.readSheet(0).build();
excelReader.read(readSheet);
// 关闭流操作
excelReader.finish();

}

测试结果

image-20240324212642997

2.2 指定列的下标或列名

当实体类的属性顺序和表中的属性顺序不一致时,需要通过注解指定列的下标或者列名

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
public class IndexOrNameData {
/**
* 强制读取第三个 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
*/
@ExcelProperty(index = 2)
private Double doubleData;
/**
* 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
*/
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
}


表字段

image-20240324214914301

测试方法

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
/**
* @description: 指定列的下标或者列的名称
*/
@Test
public void test03() {
String fileName = PATH + "user10.xlsx";

EasyExcel.read(fileName, IndexOrNameData.class, new AnalysisEventListener<IndexOrNameData>() {
/**
* @description: 每解析一行Excel表数据,就会调用一次
*/
@Override
public void invoke(IndexOrNameData data, AnalysisContext context) {
//在这里操作,将解析的每一条数据保存到数据库中,在这里可以调用数据库
System.out.println("解析的数据为: " + data);
}

/**
* @description: 解析完被调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("解析完成");
}
}).sheet().doRead();
}

测试结果

image-20240324215010853

2.3 日期或数字的格式转换

如果Excel表中的日期格式或者数字格式不符合需求,或者无法被Java解析时,需要在接收的实体类上进行数据转换

修改Excel表

image-20240324220435980

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 定义需要被格式化的字段
* 如果使用@NumberFormat("#.##")注解,建议数据类型采用String,如果使用double可能不能被格式化
*/
public class ConverterData {
@ExcelProperty("姓名")
private String name;
@NumberFormat("#.##")
@ExcelProperty("数字标题")
private String salary;
@DateTimeFormat("yyyy年MM月dd日 HH时mm分ss秒")
@ExcelProperty("日期标题")
private Date hireDate;
}

测试类

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
/**
* @description: 日期数字格式化
*/
@Test
public void test04() {
String fileName = PATH + "user10.xlsx";

EasyExcel.read(fileName, ConverterData.class, new AnalysisEventListener<ConverterData>() {
/**
* @description: 每解析一行Excel表数据,就会调用一次
*/
@Override
public void invoke(ConverterData data, AnalysisContext context) {
//在这里操作,将解析的每一条数据保存到数据库中,在这里可以调用数据库
System.out.println("解析的数据为: " + data);
}

/**
* @description: 解析完被调用
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("解析完成");
}
}).sheet().doRead();

}

测试结果

image-20240324222116773

2.4 读取多个或全部sheet标签

读取全部sheet标签

只需要在构建时使用doReadAll()即可,这里要注意要所有的sheet都是使用同一个类模板来读取,才能使用doReadAll()

image-20240324222829772


读取部分sheet

使用简单读方式二,每次创建不同的sheet


示例代码

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
65
66
67
68
/*
* 读取Excel中的多个或者全部sheet
*/
@Test
public void testRead5() {
//读取的文件路径
String fileName="user11.xlsx";
Class<DemoData> head = DemoData.class; //创建一个数据格式来承装读取到数据
/**
* 读取全部sheet
*/
EasyExcel.read(fileName, head, new AnalysisEventListener<DemoData>() {

@Override
public void invoke(DemoData data, AnalysisContext context) {
// 每解析一条数据被调用一次
System.out.println("解析的数据为: "+data);
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 数据解析完成之后被调用
System.out.println("数据解析完成......");
}
}).doReadAll();

System.out.println("============================================================");

/**
* 读取其中的某几个sheet
*/
//构建ExcelReader对象
ExcelReader excelReader = EasyExcel.read(fileName).build();
//创建想要获取的sheet对象
ReadSheet sheet1 = EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new AnalysisEventListener<DemoData>() {

@Override
public void invoke(DemoData data, AnalysisContext context) {
// 每解析一条数据被调用一次
System.out.println("解析的数据为: "+data);
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 数据解析完成之后被调用
System.out.println("数据解析完成......");
}
}).build();

ReadSheet sheet2 = EasyExcel.readSheet(2).head(DemoData.class).registerReadListener(new AnalysisEventListener<DemoData>() {

@Override
public void invoke(DemoData data, AnalysisContext context) {
// 每解析一条数据被调用一次
System.out.println("解析的数据为: "+data);
}

@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 数据解析完成之后被调用
System.out.println("数据解析完成......");
}
}).build();
//读取sheet
excelReader.read(sheet1,sheet2);
//关闭
excelReader.finish();
}

官方代码

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
/**
* 读多个或者全部sheet,这里注意一个sheet不能读取多次,多次读取需要重新读取文件
* <p>
* 1. 创建excel对应的实体对象 参照{@link DemoData}
* <p>
* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
* <p>
* 3. 直接读即可
*/
@Test
public void repeatedRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 读取全部sheet
// 这里需要注意 DemoDataListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个DemoDataListener里面写
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).doReadAll();

// 读取部分sheet
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";

// 写法1
try (ExcelReader excelReader = EasyExcel.read(fileName).build()) {
// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
ReadSheet readSheet1 =
EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
ReadSheet readSheet2 =
EasyExcel.readSheet(1).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
}
}


3. 填充

3.1 简单填充,填充当个对象

填充单个属性使用{}作为占位符,在大括号里面定义属性名称,如果{}想不作为占位符展示出来,可以使用反斜杠进行转义.

填充模板excel

image-20240325125507508

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FullTest {
public String PATH = "E:\\workspace\\esayexcel\\";

/**
* @description: 单个对象填充
*/
@Test
public void test01() {
// 根据哪个模板填充
String templateName = PATH + "templateName.xlsx";
// 填充完成后的excel
String fullName = PATH + "fullFileName.xlsx";
// 构建数据
FillData data = new FillData();
data.setName("小白");
data.setNumber(1111.22);

EasyExcel.write(fullName).withTemplate(templateName).sheet().doFill(data);
}
}

测试结果

image-20240325125608289

3.2 填充列表数据

excel填充模板

image-20240325130207466

测试方法

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
/**
* @description: 填充列表数据
*/
@Test
public void test02() {
// 根据哪个模板填充
String templateName = PATH + "templateName2.xlsx";
// 填充完成后的excel
String fullName = PATH + "fullFileName2.xlsx";
// 构建数据
List<FillData> list = new ArrayList<>();
FillData data1 = new FillData();
data1.setName("小白");
data1.setNumber(1111.22);
FillData data2 = new FillData();
data2.setName("小红");
data2.setNumber(11312.22);
FillData data3 = new FillData();
data3.setName("小王");
data3.setNumber(123124.123);
list.add(data1);
list.add(data2);
list.add(data3);

EasyExcel.write(fullName).withTemplate(templateName).sheet().doFill(list);

}

测试结果

image-20240325130308037

4.Web操作

4.1 文件下载

这里是我在完善个人学习项目功能时写的,这里只截取了一部分前端代码,通过前端处理来导出excel


导入xlsx插件

在package.json中导入依赖

拉取依赖,npm insatll 或者 npm install xlsx –save

image-20240326145431238


写导出按钮

image-20240326145649180

写函数

先在script中引入xlsx

image-20240326145747938

submit函数

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
submit() {
if (this.tableData.length <= 0) {
this.$message({
message: "没有成绩,无法导出",
type: "warning",
showClose: true
});
return;
}
let arr = this.tableData.map(item => {
return {
课程名: item.courseName,
教师: item.teacherName,
学分: item.credit,
日常成绩: item.dailyScore,
考试成绩: item.examScore,
总成绩: item.score
};
});
let sheet = xlsx.utils.json_to_sheet(arr),
book = xlsx.utils.book_new();
xlsx.utils.book_append_sheet(book, sheet, "成绩表");
xlsx.writeFile(book, `成绩表${new Date().getTime()}.xls`);
this.disabled = false;
}

数据

我这里只有成绩数据,并且不需要经过筛选,所以直接全部导出查询到的成绩数据,也就是tableData

image-20240326150520360


测试

前端效果

image-20240326150719647

导出成绩

image-20240326150817272 image-20240326150854696

如果不存在成绩

image-20240326150953313

Apache poi 和 EasyExcel
https://lzhengjy.github.io/2024/03/16/EasyExcel/
作者
Zheng
发布于
2024年3月16日
许可协议