初识 EasyExcel-转载

EasyExcel

 转载

mob604756ef7d062021-09-15 15:28:00

文章标签数据实体类解决方法apache封装文章分类代码人生阅读数1276

EasyExcel

一、初识 EasyExcel

1. Apache POI

​Apache POI​​是Apache软件基金会的开源小项目,它提供了 Java 的 API 来实现对​​Microsoft Office​​(word、excel、ppt)格式档案的读写。但是存在如下一些问题:

  • 学习使用成本较高

1、对 POI 有过深入了解的才知道原来 POI 还有 SAX模式(相对于Dom解析模式)。

Dom解析模式:一次性把文档加载到内存中,内存消耗巨大

SAX模式:一行行去读文档,算是Dom解析模式的优化

2、但SAX模式相对比较复杂,excel有03(xls)和07(xlsx)两种版本,两个版本数据存储方式截然不同,sax解析方式也各不一样。(版本不同解析的方法也不同,学习成本再一次提高)

3、想要了解清楚这两种解析方式,才去写代码测试,估计两天时间是需要的。再加上即使解析完,要转换到自己业务模型还要很多繁琐的代码。总体下来感觉至少需要三天,由于代码复杂,后续维护成本巨大。(学习和维护成本都很高)

4、POI的SAX模式的API可以一定程度的解决一些内存溢出的问题,但是POI还是有一些缺陷,比如07版Excel解压缩以及解压后存储都是在内存中完成的,内存消耗依然很大,一个3M的Excel用POI的SAX解析,依然需要100M左右内存。(内存消耗07版的依旧很大)

  • POI的内存消耗较大

大部分使用POI都是使用他的 userModel 模式。userModel 的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,相对比较好理解。然而 userModel 模式最大的问题是在于非常大的内存消耗,一个几兆的文件解析要用掉上百兆的内存。现在很多应用采用这种模式,之所以还正常在跑一定是并发不大,并发上来后一定会OOM或者频繁的full gc。(想要快速上手,内存消耗就非常大,高并发环境下内存很容易溢出)

总体上来说,简单写法重度依赖内存,复杂写法学习成本高。但是POI的功能还是特别丰富强大的。

2. EasyExcel

1、EasyExcel 重写了 POI 对07版 Excel 的解析,可以把内存消耗从100M 左右降低到 10M 以内,并且再大的 Excel 不会出现内存溢出。(内存消耗少了一个数量级)

2、但 EasyExcel 对 03 版的 Excel 仍使用 POI 的 SAX 模式。

3、EasyExcel 的效率也很高,在 64M 内存环境下,读取 75M (46W行25列)的 Excel 能在 1 分钟内跑完。(性能不错)

4、在上层做了模型转换的封装,让使用者更加简单方便。(使用简单)

总体上来说,使用简单,内存消耗低(可以有效避免OOM),但只能操作 Excel,且不能读取图片。

二、快速入门

我们通常读取 Excel 中的内容使用对应的实体类进行封装,最终再存入数据库。( Excel 的读 )

或者将数据库的数据使用对应的实体类进行封装,再写入 Excel。( Excel 的写 )

1. Excel 的读

1、引入依赖,easyexcel、lombok

 
 

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>

发现 easyexcel 依赖包含了 apache 的 poi

2、 编写实体类,和 Excel 数据对应

 
 

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String id;
private String name;
private String gender;
private Date birthday;
}

3、编写一个测试类,编写简单的读 Excel 的 Demo

 
 

@Test
public void testReadExcel() {
/*
获得一个工作簿读对象
参数一:Excel文件路径
参数二:每行数据对应的实体类型
参数三:读监听器,每读一行就会调用一次该监听器的invoke方法
*/
ExcelReaderBuilder readWorkBook = EasyExcel.read("E:/学生信息.xlsx", Student.class, new StudentReadListener());

// 获得一个工作表对象
ExcelReaderSheetBuilder sheet = readWorkBook.sheet();

// 读取工作表中的内容
sheet.doRead();

}

EasyExcel.read 方法需要一个读监听器去处理每次读出的一行数据,每读一行就触发一次 invoke 方法,将读到的数据封装成实体传入到参数中。

 
 

public class StudentReadListener extends AnalysisEventListener<Student> {

/**
* EasyExcel 每读一行就会调用一次此方法,把读到的数据存入 student 中
* @param student 每读一行的数据
* @param analysisContext
*/
@Override
public void invoke(Student student, AnalysisContext analysisContext) {
System.out.println(student);
}

/**
* 读取完整个文档之后调用的方法
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("Excel全部读取完毕了!");
}
}

4、运行读 Excel的 Demo,查看结果

2. Excel 的写

1、编写一个测试类,编写简单的写 Excel 的 Demo

 
 

@Test
public void testWriteExcel() {

/*
获得一个工作簿写对象
参数一:导出的Excel文件路径
参数二:每行数据对应的实体类型
*/
ExcelWriterBuilder writeWorkBook = EasyExcel.write("E:/学生信息2.xlsx", Student.class);
// 获得一个工作表对象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准备学生数据
List<Student> students = initData();
// 写入工作表
sheet.doWrite(students);
}


private List<Student> initData(){
List<Student> students = new ArrayList<>();

for (int i = 1; i <= 10; i++) {
Student student =new Student();
student.setName("学员"+i);
student.setBirthday(new Date());
student.setGender("男");
students.add(student);
}
return students;
}

2、运行写 Excel 的 Demo,查看结果

问题:

  • 是否能隐藏id列?
  • 表头的名称是否能修改?
  • 表头的列宽能否改变?
  • 每列的顺序能否调换?

三、问题优化

1. 表头的名称修改

1、解决方法:实体类属性的名字决定了表头的名称,在实体类属性上添加 ​​@ExcelProperty​​ 注解,并指定对应名称的 value。

 
 

public class Student {
@ExcelProperty(value = "ID") // 修改表头的名称
private String id;
@ExcelProperty(value = "学生姓名") // 修改表头的名称
private String name;
@ExcelProperty(value = "学生性别") // 修改表头的名称
private String gender;
@ExcelProperty(value = "学生生日") // 修改表头的名称
private Date birthday;
}

2、再次运行写Excel的Demo,结果如下:

2. 表头的列宽修改

1、解决方法:在实体类属性上添加 ​​@ColumnWidth​​ 注解,值为 Excel 表中的列宽大小。

 
 

public class Student {
@ExcelProperty(value = "ID")
private String id;
@ExcelProperty(value = "学生姓名")
@ColumnWidth(20) // 修改表头的列宽
private String name;
@ExcelProperty(value = "学生性别")
@ColumnWidth(20) // 修改表头的列宽
private String gender;
@ExcelProperty(value = "学生生日")
@ColumnWidth(20) // 修改表头的列宽
private Date birthday;
}

2、再次运行写Excel的Demo,结果如下:

也可以在类上使用 ​​@ColumnWidth​​ 注解,整个表头相同列宽。

3. 行高的修改

1、可以修改 Excel 表中内容的行高,在类上添加 ​​@ContentRowHeight​​ 注解,值为内容行高的大小。

2、可以修改表头的行高,在类上添加 ​​@HeadRowHeight​​ 注解,值为表头行高的大小。

 
 

@ContentRowHeight(10) // 修改 Excel 表中内容的行高
@HeadRowHeight(15) // 修改表头的行高
public class Student {
@ExcelProperty(value = "ID")
private String id;
@ExcelProperty(value = "学生姓名")
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "学生性别")
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "学生生日")
@ColumnWidth(20)
private Date birthday;
}

3、再次运行写Excel的Demo,结果如下:

4. 修改列的顺序

1、在实体类的属性的 ​​@ExcelProperty​​ 注解里,添加一个属性 index ,对应了列的位置。

注意:index对应的是列的位置,从0开始。

如果index从1开始写,在excel表中的第一列会被空出来。

如果index写为5,在excel表中就会放在第6列。

 
 

@ContentRowHeight(10)
@HeadRowHeight(15)
public class Student {
@ExcelProperty(value = "ID", index = 3)
private String id;
@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 birthday;
}

2、再次运行写Excel的Demo,结果如下:

5. 关于Excel读

1、如果未指定 ​​@ExcelProperty​​ 注解,会将 Excel 表中从左到右列的值,分别读取到实体类的从上到下的属性中。

2、如果实体类的属性上指定了​​@ExcelProperty(value = "学生姓名", index = 0) ​​注解,会根据 value 值去寻找表头名和“学生姓名”相同的列的值存入实体类的属性中;或者可以根据 index 去寻找第1(0+1)的列的值存入实体类的属性中。

如果是 Excel 读的实体类的话,建议要么所有的属性都不加 ​​@ExcelProperty​​ 注解,要么全用​​ @ExcelProperty(value="xxx")​​通过 value 值匹配,要么全加​​@ExcelProperty(index=x)​​通过列数去匹配,建议不要三种混合用。

6. 指定列隐藏

1、解决方法:在实体类需要需要隐藏的属性上添加 ​​@ExcelIgnore​​ 注解,这一个数据就不会写入 Excel 和读取 Excel 了。

 
 

@ContentRowHeight(10)
@HeadRowHeight(15)
public class Student {
@ExcelIgnore // 忽略属性,不参与读写
private String id;
@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 birthday;
}

2、再次运行写Excel的Demo,结果如下:

也可以在类上添加 ​​@ExcelIgnoreUnannotated​​ 注解,添加这个注解后,只有实体类属性上有 @ExcelProperty 注解才会参与读写。

7. 日期格式的转换

1、解决方法:在实体类的日期属性上添加 ​​@DateTimeFormat​​ 注解,值为日期格式。

 
 

@ContentRowHeight(10)
@HeadRowHeight(15)
public class Student {
@ExcelProperty(value = "ID", index = 3)
@ExcelIgnore
private String id;
@ExcelProperty(value = "学生姓名", index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "学生性别", index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "学生生日", index = 2)
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd") // 修改日期格式
private Date birthday;
}

2、再次运行写Excel的Demo,结果如下:

数字类型的格式也可以指定,需要在实体类的数字属性上添加 ​​@NumberFormat​​ 注解,数字格式可以参考​​java.text.DecimalFormat​​。

8. 多表头的写

1、解决方法:在实体类的 ​​@ExcelProperty​​注解的value属性发现是一个数据类型,说明可以添加多个value。

2、若只在一个属性的注解上添加多个value,查看效果:

public class Student {
   
   

@ExcelIgnore
private String id;
@ExcelProperty(value = {"学生信息表","学生姓名"}, index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = "学生性别", index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = "学生生日", index = 2)
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
}

3、若在所有的属性的注解上添加多个value,查看效果,发现表头自动合并了,可见相同value的表头属性会自动合并:

 
 

public class Student {
@ExcelIgnore
private String id;
@ExcelProperty(value = {"学生信息表","学生姓名"}, index = 0)
@ColumnWidth(20)
private String name;
@ExcelProperty(value = {"学生信息表","学生性别"}, index = 1)
@ColumnWidth(20)
private String gender;
@ExcelProperty(value = {"学生信息表","学生生日"}, index = 2)
@ColumnWidth(20)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
}

读取多表头的 Excel 时,可以使用 ExcelReaderBuilder 中的 headRowNumber(Integer num) 方法,跳过前 num 行读取数据。

如果设置 2,则跳过前两行表头从第三行开始读取数据。

四、报表的填充

1、EasyExcel支持调整行高、列宽、背景色、字体大小等内容,但是控制方式与使用原生POI无异,比较繁琐,不建议使用。

2、我们一般是先写好几套 excel 的模板,模板里面不存数据,只有报表的样式。

3、这时我们就需要使用 easyexcel 对报表数据进行填充。

1. 单个数据填充

1、准备模板

Excel表格中用{} 来包裹要填充的变量,如果单元格文本中本来就有​​{​​、​​}​​左右大括号,需要在括号前面使用斜杠转义​​\{​​、​​\}​​。

代码中用来填充数据的实体对象的成员变量名或被填充map集合的key需要和Excel中被{}包裹的变量名称一致。

模板的文件名这里名为 ​​excel_template_01.xlsx​

2、准备实体类

 
 

@Data
public class FillData {
private String name;
private int age;
}

3、编写填充 Demo

 
 

@Test
public void testFullExcel() {
// 获得工作簿对象
ExcelWriterBuilder writeWorkBook = EasyExcel.write("E:/填充一组数据.xlsx", FillData.class)
.withTemplate("E:/excel_template_01.xlsx"); // withTemplate 指定模板文件
// 获得一个工作表对象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准备填充数据
FillData fillData = new FillData();
fillData.setName("小明");
fillData.setAge(18);
// 写入工作表
sheet.doFill(fillData); // doFill 开始填充
}

2. 多组数据填充

1、准备模板

和单个数据的模板类似,只是在大括号中的前面添加一个​​.​

模板的文件名这里名为 ​​excel_template_02.xlsx​

2、编写填充 Demo

 
 

@Test
public void testFullExcel() {
ExcelWriterBuilder writeWorkBook = EasyExcel.write("E:/填充多组数据.xlsx", FillData.class)
.withTemplate("E:/excel_template_02.xlsx");
// 获得一个工作表对象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准备填充数据
List<FillData> fillDataList = this.initFillData();
// 写入工作表
sheet.doFill(fillDataList);
}

private List<FillData> initFillData() {
List<FillData> fillDataList = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
FillData fillData = new FillData();
fillData.setName("小明"+i);
fillData.setAge(18+i);
fillDataList.add(fillData);
}
return fillDataList;
}

3. 组合数据填充

又有单个数据,又有多组数据。

1、准备模板

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

2、编写填充 Demo

 
 

@Test
public void testFullExcel() {
ExcelWriter writeWorkBook = EasyExcel.write("E:/组合填充.xlsx", FillData.class)
.withTemplate("E:/excel_template_03.xlsx").build();
// 获得一个工作表对象
WriteSheet writeSheet = EasyExcel.writerSheet().build();
// 准备填充数据
List<FillData> fillDataList = this.initFillData();

Map<String,String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date","2021-9-15");
dateAndTotal.put("total","10086");

// 多组填充
writeWorkBook.fill(fillDataList,writeSheet);
// 单组填充
writeWorkBook.fill(dateAndTotal,writeSheet);
// 关闭流,切记!
writeWorkBook.finish();
}

测试发现,如果多组填充在前面,多组填充后没有新增行,导致后续单组填充时,把之前多组填充的值覆盖了!

3、可以将单个数据放在多个数据前面,或者需要设置多组填充时能够添加一行,可以通过​​FillConfig​​对象设置换行。

 
 

@Test
public void testFullExcel() {
ExcelWriter writeWorkBook = EasyExcel.write("E:/组合填充.xlsx", FillData.class)
.withTemplate("E:/excel_template_03.xlsx").build();
// 获得一个工作表对象
WriteSheet writeSheet = EasyExcel.writerSheet().build();

// 准备填充数据
List<FillData> fillDataList = this.initFillData();

Map<String, String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date", "2021-9-15");
dateAndTotal.put("total", "10086");

// 填充后换行
FillConfig fillConfig = FillConfig.builder().forceNewRow(true).build();
// 多组填充,填充后要换行
writeWorkBook.fill(fillDataList, fillConfig, writeSheet);
// 单组填充
writeWorkBook.fill(dateAndTotal, writeSheet);
// 关闭流,切记!
writeWorkBook.finish();
}

4. 水平数据填充

数据向右水平填充,而不是默认的向下。

1、准备模板

2、编写填充 Demo,可以通过​​FillConfig​​对象设置水平填充。

 
 

@Test
public void testHorizontalExcel() {
ExcelWriter writeWorkBook = EasyExcel.write("E:/水平填充.xlsx", FillData.class)
.withTemplate("E:/excel_template_04.xlsx").build();
// 获得一个工作表对象
WriteSheet writeSheet = EasyExcel.writerSheet().build();

// 准备填充数据
List<FillData> fillDataList = this.initFillData();

Map<String, String> dateAndTotal = new HashMap<>();
dateAndTotal.put("date", "2021-9-15");
dateAndTotal.put("total", "10086");

// 水平填充
FillConfig fillConfig = FillConfig.builder().direction(WriteDirectionEnum.HORIZONTAL).build();
// 多组填充,需要水平填充
writeWorkBook.fill(fillDataList, fillConfig, writeSheet);
// 单组填充
writeWorkBook.fill(dateAndTotal, writeSheet);
// 关闭流,切记!
writeWorkBook.finish();
}

本文章为转载内容,我们尊重原作者对文章享有的著作权。如有内容错误或侵权问题,欢迎原作者联系我们进行内容更正或删除文章。

猜你喜欢

转载自blog.csdn.net/zxl2016/article/details/131519451
今日推荐