EasyExcel


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();
}


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM