初识EasyExcel

Apache POI

Apache POI是Apache软件基金会的开源函式库,提供跨平台的Java API实现Microsoft Office格式档案读写。
特点

  1. 功能强大
  2. 代码书写冗余繁杂 ,学习和使用成本较高
  3. 读写大文件耗费内存较大,容易OOM

EasyExcel

GitHub地址 官网地址
EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目,在尽可能节约内存的情况下支持读写百M的Excel。
特点

  1. 在数据模型层面进行了封装,使用简单
  2. 重写了07版本的Excel的解析代码,降低内存消耗,能有效避免OOM
  3. 只能操作Excel
  4. 不能读取图片

性能测试
64M内存20秒读取75M(46W行25列)的Excel(3.0.2+版本),也有极速模式能更快,但是内存占用会在100M多一点。
![large-4261205e1a145b60a6051d57afe137d1.png](./assets/1668427614873-2a21e2a6-e3fb-40d2-a08f-9430f3faa6de.png %}

快速入门

官网地址

快速开始 https://easyexcel.opensource.alibaba.com/docs/current/
读Excel https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read
写Excel https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write

导入依赖坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- EasyExcel -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.6</version>
</dependency>
<!-- lombok 优雅编程 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>

读取Excel

准备工作

编写Excel文件,复制到resources目录下

编写实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString

// @Accessors(chain = true) 引发部分字段没法读取或者写入
@Builder

public class Student {

private String id;

// Excel表头信息 value为表头的值 index为索引位置 0为第一列
@ExcelProperty(value = "学生姓名", index = 0)
private String name;

@ExcelProperty(value = "学生性别", index = 1)
private String gender;

@ExcelProperty(value = "学生出生日期", index = 2)
private Date birthDate; // 只支持Date,不支持LocalDate和LocalDateTime

}

读取Excel文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 读取Excel
public class Excel_Read {
public static void main(String[] args) {
// 1.文件的路径 2.实体类的Class 3.读监听器读
// 获取Resource下的文件
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("student.xls");
EasyExcel.read(
inputStream,
Student.class,
new StudentListener()
)
.sheet() // 默认为表格1
.doRead(); // 开始读取
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Student读监听器
public class StudentListener extends AnalysisEventListener<Student> {

// 读取excel表头信息,headMap即为表头信息
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头信息:" + headMap);
}

// 读取完一行调用一次
@Override
public void invoke(Student student, AnalysisContext analysisContext) {
System.out.println("表体信息:" + student);
}

// 文档读取完成后执行
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("StudentExcel读取完毕");
}
}

读取完成

写入Excel

创建写入文件夹

工程路径下/down文件夹

编写实体类

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
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString

// @Accessors(chain = true) 引发部分字段没法读取或者写入
@Builder

public class Student {

// 写入时候忽略该字段
@ExcelIgnore
private String id;

// Excel表头信息 value为表头的值 index为Excel表的索引位置 0为第一列
@ExcelProperty(value = "学生姓名", index = 0)
private String name;

@ExcelProperty(value = "学生性别", index = 1)
private String gender;

@ExcelProperty(value = "学生出生日期", index = 2)
private Date birthDate; // 只支持Date,不支持LocalDate和LocalDateTime

}

写入Excel文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 写Excel
public class Excel_Write {
public static void main(String[] args) {

// 准备数据
ArrayList<Student> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build();
students.add(student);
}

// 1.文件的路径 2.实体类的Class
// 写入工程下down文件夹中
EasyExcel.write(
"down/student_Write.xls",
Student.class
).sheet() // 默认为表格1
.doWrite(students);
}
}

写入完成

写入带有表头的Excel

创建写入文件夹

工程路径下/down文件夹

编写实体类

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
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString

// @Accessors(chain = true) 引发部分字段没法读取或者写入
@Builder

public class Student {

// 写入时候忽略该字段
@ExcelIgnore
private String id;

// Excel表头信息 value为表头的值 index为Excel表的索引位置 0为第一列
@ExcelProperty(value = {"学生信息表", "学生姓名"}, index = 0)
private String name;

@ExcelProperty(value = {"学生信息表", "学生性别"}, index = 1)
private String gender;

@ExcelProperty(value = {"学生信息表", "学生出生日期"}, index = 2)
@DateTimeFormat("yyyy-MM-dd") // 将Date格式转化为指定格式
private Date birthDate; // 只支持Date,不支持LocalDate和LocalDateTime

}

写入Excel文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 写Excel
public class Excel_Write {
public static void main(String[] args) {

// 准备数据
ArrayList<Student> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build();
students.add(student);
}

// 1.文件的路径 2.实体类的Class
// 写入工程下down文件夹中
EasyExcel.write(
"down/student_Write.xls",
Student.class
).sheet() // 默认为表格1
.doWrite(students);
}
}

写入完成

写入带有单元格样式的Excel

EasyExcel支持调整行高、列宽、背景色、字体大小等内容,控制方式和使用原生POI无异,比较繁琐,不建议使用。推荐使用模板填充的方式,向预设样式的表格中直接写入数据,写入数据的时候会保持原有样式。

常见注解

1
2
3
4
5
6
// 表头行高    作用于类
@HeadRowHeight(25)
// 内容行高 作用于类
@ContentRowHeight(15)
// 列宽 作用于全部 作用于类和字段
@ColumnWidth(20)

创建写入文件夹

工程路径下/down文件夹

修改实体类

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
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString

// @Accessors(chain = true) 引发部分字段没法读取或者写入
@Builder

// 表头行高
@HeadRowHeight(25)
// 内容行高
@ContentRowHeight(15)
// 列宽 作用于全部
@ColumnWidth(20)
public class Student {

// 写入时候忽略该字段
@ExcelIgnore
private String id;

// Excel表头信息 value为表头的值 index为Excel表的索引位置
@ExcelProperty(value = "学生姓名", index = 0)
// 列宽
@ColumnWidth(20)
private String name;

@ExcelProperty(value = "学生性别", index = 1)
@ColumnWidth(20)
private String gender;

@ExcelProperty(value = "学生出生日期", index = 2)
@ColumnWidth(20)
private Date birthDate; // 只支持Date,不支持LocalDate和LocalDateTime

}

重新写入Excel文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 写Excel
public class Excel_Write {
public static void main(String[] args) {

// 准备数据
ArrayList<Student> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build();
students.add(student);
}

// 1.文件的路径 2.实体类的Class
// 写入工程下down文件夹中
EasyExcel.write(
"down/student_Write.xls",
Student.class
).sheet() // 默认为表格1
.doWrite(students);
}
}

写入完成

文件上传和下载

Excel文件上传

准备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
27
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString

// @Accessors(chain = true) 引发部分字段没法读取或者写入
@Builder

public class Student {

// 写入时候忽略该字段
@ExcelIgnore
private String id;

// Excel表头信息 value为表头的值 index为Excel表的索引位置 0为第一列
@ExcelProperty(value = "学生姓名", index = 0)
private String name;

@ExcelProperty(value = "学生性别", index = 1)
private String gender;

@ExcelProperty(value = "学生出生日期", index = 2)
@DateTimeFormat("yyyy/MM/dd") // 将Date格式转化为指定格式
private Date birthDate; // 只支持Date,不支持LocalDate和LocalDateTime

}

编写数据处理逻辑

1
2
3
4
5
6
7
8
9
@Service("studentService")
public class StudentService {
// 调用数据库进行插入
public void readExcel(List<Student> students) {
students.forEach(student -> {
System.out.println("插入" + student);
});
}
}

编写读监听器

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
// 读监听器
@Component
@Scope("prototype") // 必须为多例
public class ExcelListener extends AnalysisEventListener<Student> {

// 存储读取的Excel信息
private ArrayList<Student> students = new ArrayList<>();

// 调用服务插入数据库
private final StudentService studentService;

public ExcelListener(StudentService studentService) {
this.studentService = studentService;
}

// 读取excel表头信息,headMap即为表头信息
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
System.out.println("表头信息:" + headMap);
}

// 读取完一行调用一次,通常在此次存入数据库
@Override
public void invoke(Student student, AnalysisContext analysisContext) {
// 存入容器
students.add(student);
// 插入数据库
if (students.size() % 5 == 0) {
studentService.readExcel(students);
students.clear();
}
}

// 文档读取完成后执行
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("StudentExcel读取完毕");
}
}

编写控制器读取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
27
28
29
30
// 处理web环境的Excel文件
@Controller("excelController")
@RequestMapping("/excel")
public class ExcelController {

// 注入Excel读取监听器
private final ExcelListener excelListener;

public ExcelController(ExcelListener excelListener) {
this.excelListener = excelListener;
}

// 处理上传的文件
@ResponseBody
@PostMapping("/read")
public Map<String, String> readExcel(@RequestPart("file") MultipartFile file) {
HashMap<String, String> map = new HashMap<>();
try {
// 读取Excel文件
ExcelReaderBuilder read = EasyExcel.read(file.getInputStream(), Student.class, excelListener);
ExcelReaderSheetBuilder sheet = read.sheet();
sheet.doRead();
map.put("message", "success");
} catch (IOException e) {
map.put("message", "error");
throw new RuntimeException(e);
}
return map;
}
}

测试案例

http://localhost:9999/excel/read
类型 Content-Type-multipart/form-data

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
27
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString

// @Accessors(chain = true) 引发部分字段没法读取或者写入
@Builder

public class Student {

// 写入时候忽略该字段
@ExcelIgnore
private String id;

// Excel表头信息 value为表头的值 index为Excel表的索引位置 0为第一列
@ExcelProperty(value = "学生姓名", index = 0)
private String name;

@ExcelProperty(value = "学生性别", index = 1)
private String gender;

@ExcelProperty(value = "学生出生日期", index = 2)
@DateTimeFormat("yyyy/MM/dd") // 将Date格式转化为指定格式
private Date birthDate; // 只支持Date,不支持LocalDate和LocalDateTime

}

编写控制器写入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
// 处理web环境的Excel文件
@Controller("excelController")
@RequestMapping("/excel")
public class ExcelController {

@RequestMapping(value = "/write")
public void write(HttpServletResponse response) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\\+", "%20");
response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), Student.class).sheet().doWrite(data());
}

private List<Student> data() {
// 准备数据
ArrayList<Student> students = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
Student student = Student.builder().name("Ylan" + i).gender("男").birthDate(date).build();
students.add(student);
}
return students;
}
}

测试案例

http://localhost:9999/excel/write

填充模板

官方地址

模板填充 https://easyexcel.opensource.alibaba.com/docs/current/quickstart/fill#since-3

准备工作

创建写入文件夹

工程路径下/down文件夹

填充单组数据

准备模板

Excel表格中用**{}包裹表示要填充的变量,如果单元格文本中本来就有{},需要在括号前面使用斜杠转义{}
代码中被填充
实体对象的成员变量名或被填充map集合的key**需要和Excel中被{}包裹的变量名称一致。

封装数据

1
2
3
4
5
6
7
8
9
10
// 使用实体类封装填充数据
@Data
@Builder
public class FileData {
private String name;
private int age;
}

// 准备数据 实体类
FileData data = FileData.builder().name("Ylan").age(21).build();
1
2
3
4
// 准备数据 Map类
HashMap<String, String> data = new HashMap<>();
data.put("name", "pepsi-wyl");
data.put("age", "21");

开始填充

准备数据并填充到文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 单组填充
public class TemplateTest1 {
public static void main(String[] args) {
// 准备模板
InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template1.xlsx");
// 准备输入文件
String targetFileName = "down/Excel_填充_单组数据.xlsx";
// 准备数据 实体类
FileData data = FileData.builder().name("Ylan").age(21).build();
// 准备数据 Map类
// HashMap<String, String> data = new HashMap<>();
// data.put("name", "pepsi-wyl");
// data.put("age", "21");

// 创建工作簿对象 关联模板和输入文件
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(targetFileName, FileData.class).withTemplate(template);
// 创建工资表对象
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
// 填充数据 关联数据
sheet.doFill(data);
}
}

填充效果

填充多组数据

准备模板

Excel表格中用 {.} 包裹表示要填充的变量,如果单元格文本中本来就有**{},需要在括号前面使用斜杠转义{}
代码中被填充
实体对象的成员变量名或被填充map集合的key**需要和Excel中被{}包裹的变量名称一致。

封装数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用实体类封装填充数据
@Data
@Builder
public class FileData {
private String name;
private int age;
}

// 准备数据 实体类
private static List<FileData> initFillData() {
ArrayList<FileData> fillDatas = new ArrayList<FileData>();
for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build());
return fillDatas;
}
1
// 同上

开始填充

准备数据并填充到文件

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 TemplateTest2 {
public static void main(String[] args) {
// 准备模板
InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template2.xlsx");
// 准备输入文件
String targetFileName = "down/Excel_填充_多组数据.xlsx";
// 准备数据 实体类集合
List<FileData> dataList = initFillData();

// 创建工作簿对象 关联模板和输入文件
ExcelWriterBuilder excelWriterBuilder = EasyExcel.write(targetFileName, FileData.class).withTemplate(template);
// 创建工资表对象
ExcelWriterSheetBuilder sheet = excelWriterBuilder.sheet();
// 填充数据 关联数据
sheet.doFill(dataList);
}

private static List<FileData> initFillData() {
ArrayList<FileData> fillDatas = new ArrayList<FileData>();
for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build());
return fillDatas;
}
}

填充效果

组合填充

准备模板

即有多组数据填充,又有单一数据填充,为了避免两者数据出现冲突覆盖的情况,在多组填充时需要通过**FillConfig**对象设置换行。

封装数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用实体类封装填充数据
@Data
@Builder
public class FileData {
private String name;
private int age;
}

// 准备数据 实体类
private static List<FileData> initFillData() {
ArrayList<FileData> fillDatas = new ArrayList<FileData>();
for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build());
return fillDatas;
}
1
2
3
4
// 准备数据 Map类
HashMap<String, String> dataMap = new HashMap<>();
dataMap.put("date", "2022-03-17");
dataMap.put("total", "21");

开始填充

准备数据并填充到文件

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
// 组合填充
public class TemplateTest3 {
public static void main(String[] args) {
// 准备模板
InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template3.xlsx");
// 准备输入文件
String targetFileName = "down/Excel_填充_组合数据.xlsx";
// 准备数据 实体类
List<FileData> dataList = initFillData();
// 准备数据 Map类
HashMap<String, String> dataMap = new HashMap<>();
dataMap.put("date", "2022-03-17");
dataMap.put("total", "21");

// 创建工作簿对象 关联模板和输入文件
ExcelWriter workBook = EasyExcel.write(targetFileName, FileData.class).withTemplate(template).build();
// 创建工资表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// 组合填充时,因为多组填充的数据量不确定,需要在多组填充完之后另起一行
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 填充数据 关联数据
workBook.fill(dataList, fillConfig, sheet); // 多组数据
workBook.fill(dataMap, sheet); // 单组数据
// 关闭流!!!
workBook.finish();
}

private static List<FileData> initFillData() {
ArrayList<FileData> fillDatas = new ArrayList<FileData>();
for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build());
return fillDatas;
}
}

填充效果

水平填充

准备模板

水平填充和多组填充模板一样,不一样的地方在于,填充时需要通过**FillConfig**对象设置水平填充。

封装数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用实体类封装填充数据
@Data
@Builder
public class FileData {
private String name;
private int age;
}

// 准备数据 实体类
private static List<FileData> initFillData() {
ArrayList<FileData> fillDatas = new ArrayList<FileData>();
for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build());
return fillDatas;
}
1
// 同上

开始填充

准备数据并填充到文件

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
// 水平填充
public class TemplateTest4 {
public static void main(String[] args) {
// 准备模板
InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/fill_data_template4.xlsx");
// 准备输入文件
String targetFileName = "down/Excel_填充_水平数据.xlsx";
// 准备数据 实体类
List<FileData> dataList = initFillData();

// 创建工作簿对象 关联模板和输入文件
ExcelWriter workBook = EasyExcel.write(targetFileName, FileData.class).withTemplate(template).build();
// 创建工资表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// 水平填充配置
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 填充数据 关联数据
workBook.fill(dataList, fillConfig, sheet); // 单组数据
// 关闭流!!!
workBook.finish();
}

private static List<FileData> initFillData() {
ArrayList<FileData> fillDatas = new ArrayList<FileData>();
for (int i = 0; i < 10; i++) fillDatas.add(FileData.builder().name("YLan" + i).age(10 + i).build());
return fillDatas;
}
}

填充效果

填充综合练习

准备模板

封装数据

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
// 会员实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
@ToString

// @Accessors(chain = true) 引发部分字段没法读取或者写入
@Builder
public class Members {

// 写入时候忽略该字段
@ExcelIgnore
private String id;

// index为Excel表的索引位置 0为第一列
@ExcelProperty(index = 0)
private String name;

@ExcelProperty(index = 1)
private String gender;

@ExcelProperty(index = 2)
private Date birthday; // 只支持Date,不支持LocalDate和LocalDateTime
}

// 封装实体类数据
private static List<Members> initData() {
ArrayList<Members> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
Members members = Members.builder().name("Ylan" + i).gender("男").birthday(date).build();
list.add(members);
}
return list;
}

// 封装Map类数据
// ****** 准备数据 *******
HashMap<String, String> data = new HashMap<String, String>();
// 日期
data.put("date", "2020-03-16");
// 总会员数
data.put("totalCount", "1000");
// 新增员数
data.put("increaseCount", "100");
// 本周新增会员数
data.put("increaseCountWeek", "50");
// 本月新增会员数
data.put("increaseCountMonth", "100");
// 新增会员数据
List<Members> members = initData();
// **** 准备数据结束****

开始填充

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
// 报表导出
public class TemplateTest5 {
public static void main(String[] args) {

// 准备模板
InputStream template = Thread.currentThread().getContextClassLoader().getResourceAsStream("template/report_template.xlsx");
// 准备输入文件
String targetFileName = "down/Excel_填充_报表导出.xlsx";

// ****** 准备数据 *******
HashMap<String, String> data = new HashMap<String, String>();
// 日期
data.put("date", "2020-03-16");
// 总会员数
data.put("totalCount", "1000");
// 新增员数
data.put("increaseCount", "100");
// 本周新增会员数
data.put("increaseCountWeek", "50");
// 本月新增会员数
data.put("increaseCountMonth", "100");
// 新增会员数据
List<Members> members = initData();
// **** 准备数据结束****

// 创建工作簿对象 关联模板和输入文件
ExcelWriter workBook = EasyExcel.write(targetFileName, FileData.class).withTemplate(template).build();
// 创建工资表对象
WriteSheet sheet = EasyExcel.writerSheet().build();
// 填充数据 关联数据
workBook.fill(data, sheet); // 单组数据
workBook.fill(members, sheet); // 多组数据
// 关闭流!!!
workBook.finish();
}

private static List<Members> initData() {
ArrayList<Members> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Date date = Date.from(LocalDate.of(2001, i + 1, 17).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant());
Members members = Members.builder().name("Ylan" + i).gender("男").birthday(date).build();
list.add(members);
}
return list;
}
}

填充效果

注意事项

为了节省内存,所以没有采用把整个文档在内存中组织好之后再整体写入到文件的做法,而是采用的是一行一行写入的方式,不能实现删除和移动行,也不支持备注写入。多组数据写入的时候,如果需要新增行,只能在最后一行增加,不能在中间位置添加。

API及注解

官方地址

读Excel https://easyexcel.opensource.alibaba.com/docs/current/api/#readsheet
写Excel https://easyexcel.opensource.alibaba.com/docs/current/api/write
填充Excel https://easyexcel.opensource.alibaba.com/docs/current/api/fill

常用类

  • EasyExcel 入口类,构建开始的操作
  • ExcelReaderBuilder 构建出一个ReadWorkbook对象,即一个工作簿对象,对应的是一个Excel文件
  • ExcelReaderSheetBuilder 构建出一个ReadSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表
  • ReadListener 在每一行读取完毕后调用ReadListener来处理数据,可以把调用service的代码可以写在其invoke方法内部
  • ExcelWriterBuilder 构建出一个WriteWorkbook对象,即一个工作簿对象,对应的是一个Excel文件
  • ExcelWriterSheetBuilder 构建出一WriteSheet对象,即一个工作表的对象,对应的Excel中的每个sheet,一个工作簿可以有多个工作表
  • WriteHandler 创建单元格、创建表格等都会调用WriteHandler来处理数据,对使用者透明不可见

所有配置都是继承的 Workbook的配置会被Sheet继承,即在设置参数的时候,在EasyExcel…sheet()方法之前作用域是整个sheet,之后针对单个sheet。

读取API及注解

注解

@ExcelProperty

使用位置:作用在成员变量上
可选属性:

属性名 含义 说明
index 对应Excel表中的列数 默认-1,指定时从0开始
value 对应Excel表中的列头
converter 成员变量转换器 自定义转换器需要实Converter接口

不使用@ExcelProperty注解时,成员变量从上到下的顺序,对应表格中从左到右的顺序进行解析。index属性和value属性都可以指定当前字段对应excel中的哪一列,推荐让index和vlaue的配置都指向同一列。

@ExcelIgnore

使用位置:标注在成员变量上
默认所有字段都会和Excel表格去匹配,加该注解后会忽略该字段,通常将数据库的主键ID加上该注解。

@DateTimeFormat

使用位置:标注在成员变量上
用于日期转换,用String类型的成员变量去接收excel中日期格式的数据会调用这个注解,value参照java.text.SimpleDateFormat格式。

@NumberFormat

使用位置:标注在成员变量上
用于数字转换,用String类型的成员变量去接收excel数字格式的数据会调用这个注解,value参照java.text.DecimalFormat格式。

@ExcelIgnoreUnannotated

使用位置:标注在类上
不标注该注解时,类中所有属性(除@ExcelIgnore的属性)都会参与读写,无论是否在成员变量上加@ExcelProperty 注解。
标注该注解后,只读写类中有标注@ExcelProperty 注解的属性。

读取时通用参数

ReadWorkbook,ReadSheet 都有的参数,为空时默认使用上级

  • converter 转换器,默认加载了很多转换器,也可以自定义
  • readListener 读监听器,在读取数据的过程中会不断的调用监听器
  • headRowNumber 指定需要读的表格的列头行数。默认有一行头,也就是认为第二行开始起为数据。
  • headclazz二选一。读取文件头对应的列表,根据列表匹配数据。建议使用class,即文件中每一行数据对应的代码中的实体类型。
  • clazzhead二选一。读取文件头对应的class,根据class匹配数据。如果两个都不指定,则会读取全部数据。
  • autoTrim 字符串、表头等数据自动trim
  • password 读的时候是否需要使用密码

ReadWorkbook(工作簿对象)参数

  • excelType 当前excel的类型,读取时自动判断,无需设置。
  • inputStreamfile二选一。建议使用file。
  • fileinputStream二选一。读取文件的文件。
  • autoCloseStream 自动关闭流。
  • readCache 默认小于5M用内存,超过5M使用 EhCache,不建议使用这个参数。
  • useDefaultListener @since 2.1.4 默认会加入ModelBuildEventListener 来帮忙转换成传入class的对象,设置成false后将不会协助转换对象,自定义的监听器会接收到Map<Integer,CellData>对象,如果还想继续接听到class对象,请调用readListener方法,加入自定义的beforeListenerModelBuildEventListener、 自定义的afterListener即可。

ReadSheet(工作表对象)参数

  • sheetNo 需要读取Sheet的编号,建议使用该参数来指定读取的Sheet
  • sheetName 需要读取Sheet的名称,excel 2003不支持根据名字去匹配

写入API及注解

注解

@ExcelProperty

使用位置:作用在成员变量上
可选属性:

属性名 含义 说明
index 对应Excel表中的列数 默认-1,指定时建议从0开始
value 对应Excel表中的列头
converter 成员变量转换器 自定义转换器需要实Converter接口

使用效果index 指定写到第几列,如果不指定则根据成员变量位置排序;value指定写入的列头,如果不指定则使用成员变量的名字作为列头;如果要设置复杂的头,可以为value指定多个值

@ExcelIgnore

使用位置:标注在成员变量上
默认所有字段都会和Excel表格去匹配,加该注解后会忽略该字段,通常将数据库的主键ID加上该注解。

@DateTimeFormat

使用位置:标注在成员变量上
用于日期转换,用String类型的成员变量去接收excel中日期格式的数据会调用这个注解,value参照java.text.SimpleDateFormat格式。

@NumberFormat

使用位置:标注在成员变量上
用于数字转换,用String类型的成员变量去接收excel数字格式的数据会调用这个注解,value参照java.text.DecimalFormat格式。

@ExcelIgnoreUnannotated

使用位置:标注在类上
不标注该注解时,类中所有属性(除@ExcelIgnore的属性)都会参与读写,无论是否在成员变量上加@ExcelProperty 注解。
标注该注解后,只读写类中有标注@ExcelProperty 注解的属性

@HeadRowHeight(value)

使用位置:标注在类上
指定列头行高

@ContentRowHeight(value)

使用位置:标注在类上上
指定内容行高,统一行高

@ColumnWidth(value)

使用位置:标注在类上或属性上
指定列宽 ,作用于类上时统一列宽,作用于属性上时单独设置列宽,属性上的优先级大于类上。

写入时通用参数

WriteWorkbookWriteSheet都有的参数,为空时使用上级

  • converter 转换器,默认加载了很多转换器,也可以自定义。
  • writeHandler 写处理器。可以实现WorkbookWriteHandler,SheetWriteHandler,RowWriteHandler,CellWriteHandler,在写入excel的不同阶段会调用,对使用者透明不可见。
  • relativeHeadRowIndex 距离多少行后开始,即开头空几行。
  • needHead 是否导出头
  • headclazz二选一。读取文件头对应的列表,根据列表匹配数据。建议使用class,即文件中每一行数据对应的代码中的实体类型。
  • clazzhead二选一。读取文件头对应的class,根据class匹配数据。如果两个都不指定,则会读取全部数据。
  • autoTrim  字符串、表头等数据自动trim

WriteWorkbook(工作簿对象)参数

  • excelType 当前excel的类型,默认为xlsx
  • outputStreamfile二选一,写入文件的流 。
  • fileoutputStream二选一,写入的文件 。
  • templateInputStream 模板的文件流
  • templateFile 模板文件
  • autoCloseStream 自动关闭流。
  • password  写的时候是否需要使用密码
  • useDefaultStyle 写的时候是否是使用默认头

WriteSheet(工作表对象)参数

  • sheetNo 需要写入Sheet的编号,建议使用该参数来指定写入的Sheet 。
  • sheetName 需要写入Sheet的名称,excel 2003不支持根据名字去匹配。

常见问题

官方地址

必读 https://easyexcel.opensource.alibaba.com/qa/
读Excel https://easyexcel.opensource.alibaba.com/qa/read
写Excel https://easyexcel.opensource.alibaba.com/qa/read
填充Excel https://easyexcel.opensource.alibaba.com/qa/fill

必读

我的部分字段为什么没法读取或者写入?

  • 读写反射对象用到了Cglib,所以成员变量必须符合驼峰规范,请确认是否符合驼峰规范
    • 在3.0.0-beta1 兼容了部分非驼峰,但是还是不建议使用非驼峰字段
  • 使用了lombok的@Accessors(chain = true) ,无法被Cglib读取
    • 建议使用@Builder来替换@Accessors(chain = true)

我在本地可以,发布到线上环境怎么不可以了?

大概率是缺少字体库导致,2个方案:

  • 安装字体(推荐)
    • 看下服务器是否安装了字体,jdk8字体需要自己安装请安装字体:dejavu-sans-fonts 和 fontconfig 在dockerfile中增加字体安装命令: RUN yum install dejavu-sans-fonts fontconfig -y
    • 普通的线上环境直接运行: yum install dejavu-sans-fonts fontconfig -y
  • 开启内存处理模式(不推荐,1W数据以内可以考虑,大了很容易OOM)
    1
    2
    3
    4
    5
    6
    EasyExcel
    .write(fileName, DemoData.class)
    // 核心这个配置 开始内存处理模式
    .inMemory(Boolean.TRUE)
    .sheet("模板")
    .doWrite(data());

读Excel

为什么Listener 不能让spring管理?

  • 在读取excel的时候,会回调com.alibaba.excel.read.listener.ReadListener#invoke的方法,而spring如果管理Listener会导致Listener 变成了单例,在有并发读取文件的情况下都会回调同一个Listener,就无法区分是哪个文件读取出来的了。

在读的时候Listener里面需要使用spring的@Autowired

  • 方案1:避开创建Listener的类

    1
    2
    3
    4
    5
    EasyExcel.read(fileName, DemoData.class, new PageReadListener<DemoData>(dataList -> {
    for (DemoData demoData : dataList) {
    log.info("读取到一条数据{}", JSON.toJSONString(demoData));
    }
    })).sheet().doRead();
  • 方案2

    • 给Listener创建成员变量,然后在构造方法里面传进去。必须不让spring管理Listener,每次读取都要new一个。

开启急速模式

急速模式可以自己开启后测试下内存的占用,如果感觉符合预期可以直接开启。
如果最大文件条数也就十几二十万,然后excel也就是十几二十M,而且不会有很高的并发,并且内存也较大。这种情况下可以考虑开启极速模式。

1
2
3
4
// 强制使用内存存储,这样大概一个20M的excel使用150M(很多临时对象,所以100M会一直GC)的内存
// 这样效率会比上面的复杂的策略高很
// 这里再说明下 就是加了个readCache(new MapCache()) 参数而已,其他的参照其他demo写 这里没有写全
EasyExcel.read().readCache(new MapCache());

写Excel

导出的excel打不开

大概率由以下2个原因导致:

  • 前端用各种框架来导出,然后把文件流改了
  • 后端各种拦截器,把文件流改了

解决方案:

  • 先测试把文件导出到本地,而不是通过浏览器
    • 这个核心定位是不是自己代码写错了,通过代表代码没问题
    • 不通过自己参照下最简单的写试试
  • 再通过浏览器访问的方案去导出
    • 这个核心确认是否被后端的各种拦截器或者nginx把流给改坏了,通过代表后端+网络没问题
    • 不通过自己试下不用nginx等自己访问行不行,如果还不行那就是后端的各种拦截器导致
      • 这里注意下注解是@Controller ,不是RestController
      • 返回是void,然后确保后面所有的拦截器之类的不能再修改流
  • 接下来就是甩锅给前端
    • 后端只要确保浏览器直接访问能下载就行,接下来前端各种框架处理的方案都不一样

填充Excel

模板中的字段未替换

使用EasyExcel生成的模板,去填充,结果填充字段未替换。这个问题原因比较复杂,感兴趣的同学可以了解下:ShardingStrings ,EasyExcel在写入的时候,为了省内存,而未使用ShardingStrings。

解决方案:

在导出模板时,使用参数inMemory=true即可。

1
2
3
4
// 这里使用inMemory(true) 会触发 写入到ShardingStrings 但是这样所有的数据都会在内存 所以容易OOM
// 但是如果我们仅仅是需要导出模板,字段不是很多,所以问题不大。
// 这里注意 导出模板的加入这个参数 实际写数据千万别加,不然容易OOM
EasyExcel.write(fileName, DemoData.class).inMemory(true).sheet("模板").doWrite(fillData())

SpringBoot读取Resource文件

最近在项目中涉及到Excle的导入功能,通常是我们定义完模板供用户下载,用户按照模板填写完后上传。这里待下载模板位置为resource/excelTemplate/test.xlsx。

方式one

1
2
ClassPathResource classPathResource = new ClassPathResource("excleTemplate/test.xlsx");
InputStream inputStream =classPathResource.getInputStream();

方式two

1
InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("excleTemplate/test.xlsx");

方式three

1
InputStream inputStream = this.getClass().getResourceAsStream("/excleTemplate/test.xlsx");