转载
mob604756ef7d062021-09-15 15:28:00
文章标签数据实体类解决方法apache封装文章分类代码人生阅读数1276
EasyExcel
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都是使用他的 userModel 模式。userModel 的好处是上手容易使用简单,随便拷贝个代码跑一下,剩下就是写业务转换了,虽然转换也要写上百行代码,相对比较好理解。然而 userModel 模式最大的问题是在于非常大的内存消耗,一个几兆的文件解析要用掉上百兆的内存。现在很多应用采用这种模式,之所以还正常在跑一定是并发不大,并发上来后一定会OOM或者频繁的full gc。(想要快速上手,内存消耗就非常大,高并发环境下内存很容易溢出)
总体上来说,简单写法重度依赖内存,复杂写法学习成本高。但是POI的功能还是特别丰富强大的。
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、引入依赖,easyexcel、lombok
发现 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
/**
* 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,查看结果
1、编写一个测试类,编写简单的写 Excel 的 Demo
@Test
public void testWriteExcel() {
/*
获得一个工作簿写对象
参数一:导出的Excel文件路径
参数二:每行数据对应的实体类型
*/
ExcelWriterBuilder writeWorkBook = EasyExcel.write("E:/学生信息2.xlsx", Student.class);
// 获得一个工作表对象
ExcelWriterSheetBuilder sheet = writeWorkBook.sheet();
// 准备学生数据
List
// 写入工作表
sheet.doWrite(students);
}
private List
List
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,查看结果
问题:
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,结果如下:
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
注解,整个表头相同列宽。
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,结果如下:
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,结果如下:
1、如果未指定 @ExcelProperty
注解,会将 Excel 表中从左到右列的值,分别读取到实体类的从上到下的属性中。
2、如果实体类的属性上指定了@ExcelProperty(value = "学生姓名", index = 0)
注解,会根据 value 值去寻找表头名和“学生姓名”相同的列的值存入实体类的属性中;或者可以根据 index 去寻找第1(0+1)的列的值存入实体类的属性中。
如果是 Excel 读的实体类的话,建议要么所有的属性都不加
@ExcelProperty
注解,要么全用 @ExcelProperty(value="xxx")
通过 value 值匹配,要么全加@ExcelProperty(index=x)
通过列数去匹配,建议不要三种混合用。
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 注解才会参与读写。
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
。
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、准备模板
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 开始填充
}
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
// 写入工作表
sheet.doFill(fillDataList);
}
private List
List
for (int i = 1; i <= 10; i++) {
FillData fillData = new FillData();
fillData.setName("小明"+i);
fillData.setAge(18+i);
fillDataList.add(fillData);
}
return fillDataList;
}
又有单个数据,又有多组数据。
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
Map
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
Map
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();
}
数据向右水平填充,而不是默认的向下。
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
Map
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();
}
本文章为转载内容,我们尊重原作者对文章享有的著作权。如有内容错误或侵权问题,欢迎原作者联系我们进行内容更正或删除文章。