EasyExcel
1.简介
传统Excel操作或者解析都是利用Apach POI
进行操作,但是使用过这个框架的人都知道,这个框架并不完美,有较多的缺陷:
- 使用步骤繁琐
- 动态写出Excel操作非常麻烦
- 对于新手来说,很难在短时间内上手
- 读写时需要占用较大的内容,当数据量大时容器发生
OOM
基于上述原因,阿里开源出一款易上手,且比较节省内存的Excel
操作框架:EasyExcel
官网文档
源码地址
2.对比
-
读取
从上图可知:
-
POI
当利用
POI
去读取Excel时,首先会将数据全部加载到内存中,然后返回给调用者当数据量比较大时,及其容易发生
OOM
-
EasyExcel
与
POI
不用的是,EasyExcel
主要是采用sax模式一行一行解析,并将一行的解析结果以观察者的模式通知处理,即使数据量较大时也不会发生OOM
,以下是其读取数据原理图这样即使数据量比较大时也不会发生
OOM
,节省了内存的开销,以下是其读取数据64M内存1分钟内读取75M(46W行25列)的Excel 内存开销图
-
-
维护
当其他开源框架去使用时,步骤复杂,
EasyExcel
上手及其简单-
其他开源框架存在一些BUG修复不及时,官方文档举了一个例子,如下:
3.API
3.1 写操作
3.1.1 简单写
-
准备工作
创建springboot项目(
easyexcel
)pom.xml
内容如下:4.0.0 org.springframework.boot spring-boot-starter-parent 2.3.7.RELEASE com.briup easyexcel 0.0.1-SNAPSHOT easyexcel Demo project for Spring Boot 1.8 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine com.alibaba easyexcel 2.2.7 org.springframework.boot spring-boot-maven-plugin 修改
application.properties
文件,内容如下:server.port=9991
新增
POJO
类package com.briup.easyexcel.pojo; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data public class Student implements Serializable { private Integer id; private String name; private Double salary; private Date birthday; }
-
简单写操作
-
操作
通过
EasyExcel
这个工具类,即可完成写操作,如下:在测试类中进行代码测试
public List
getData() { List lists = new ArrayList<>(); for(int i = 0; i <= 10; i++) { Student student = new Student(); student.setId(i + 1); student.setName("李四" + i); student.setBirthday(new Date()); student.setSalary(1500.00D); lists.add(student); } return lists; } @Test void contextLoads() { EasyExcel.write("学生信息表.xlsx", Student.class).sheet().doWrite(getData()); } 执行测试方法,结果会在本地产生一个excel文件
-
代码解释:
图中红框选中部分表示:
EasyExcel.write
表示构建一个Excel写对象,其参数含义为:- 第一个参数: 写出表格的文件名
- 第二个参数:写到表格数据类型的class对象
查看
EasyExcel
源码,其所有write
方法源码如下:public class EasyExcelFactory { /** * 构建一个Excel写对象 * * @return */ public static ExcelWriterBuilder write() { return new ExcelWriterBuilder(); } /** * 构建一个Excel写对象 * * @param file 用来写出文件对象 * * @return Excel writer builder */ public static ExcelWriterBuilder write(File file) { return write(file, null); } /** * 构建 Excel写对象 * * @param file * 用来写出的文件对象 * @param head * 写出的数据类型的class对象 * @return Excel writer builder */ public static ExcelWriterBuilder write(File file, Class head) { ExcelWriterBuilder excelWriterBuilder = new ExcelWriterBuilder(); excelWriterBuilder.file(file); if (head != null) { excelWriterBuilder.head(head); } return excelWriterBuilder; } /** * 构建Excel 写对象 * * @param pathName * 写出的文件路径名 * @return Excel writer builder */ public static ExcelWriterBuilder write(String pathName) { return write(pathName, null); } /** * 构建excel 写对象 * * @param pathName * 写出的文件路径名 * @param head * 写出数据的数据类型的class对象 * @return Excel writer builder */ public static ExcelWriterBuilder write(String pathName, Class head) { ExcelWriterBuilder excelWriterBuilder = new ExcelWriterBuilder(); excelWriterBuilder.file(pathName); if (head != null) { excelWriterBuilder.head(head); } return excelWriterBuilder; } /** * 构建excel写对象 * * @param outputStream * 写出的输出流对象 * @return Excel writer builder */ public static ExcelWriterBuilder write(OutputStream outputStream) { return write(outputStream, null); } /** * 构建excel写对象 * * @param outputStream * 写出的输出流 * @param head * 写出数据的数据类型的class对象 * @return Excel writer builder */ public static ExcelWriterBuilder write(OutputStream outputStream, Class head) { ExcelWriterBuilder excelWriterBuilder = new ExcelWriterBuilder(); excelWriterBuilder.file(outputStream); if (head != null) { excelWriterBuilder.head(head); } return excelWriterBuilder; } }
通过源码知道,在构建
Excel
写对象时可以通过多种方式构建,具体使用哪种看具体的需求。
如上图,
sheet()
代表要在excel
那个sheet
页写入数据,如果不指定,默认在第一个sheet页写入数据,其sheet页的值为:0
当然也可以手动指定在哪个
sheet
页,关于sheet
源码如下:public class ExcelWriterBuilder extends AbstractExcelWriterParameterBuilder
{ /* 选中第一个sheet页 写操作 sheet 页的值为 0 */ public ExcelWriterSheetBuilder sheet() { return sheet(null, null); } /* 选中 第一个的 sheet页 sheet 页的名字 为 sheetNo */ public ExcelWriterSheetBuilder sheet(Integer sheetNo) { return sheet(sheetNo, null); } /* 选中第一个的 sheet页 sheet 页的名字 为 sheetName */ public ExcelWriterSheetBuilder sheet(String sheetName) { return sheet(null, sheetName); } /* 选中第一个 sheet页 sheet 页的名字 为 sheetNo 或者 sheetName */ public ExcelWriterSheetBuilder sheet(Integer sheetNo, String sheetName) { ExcelWriter excelWriter = build(); ExcelWriterSheetBuilder excelWriterSheetBuilder = new ExcelWriterSheetBuilder(excelWriter); if (sheetNo != null) { excelWriterSheetBuilder.sheetNo(sheetNo); } if (sheetName != null) { excelWriterSheetBuilder.sheetName(sheetName); } return excelWriterSheetBuilder; } } doWrite
表示写出的数据,写出的数据为List
集合
-
3.1.2 复杂写
-
自定义表头
如上图,之前写出的数据,表头均为属性名,且列的顺序为类中属性的顺序,但是在实际开发过程中,表头为自定义信息,且顺序也不一定按照属性的顺序来。
因此需要自定义表头信息,具体实现如下:
修改POJO类,内容如下:
public class Student implements Serializable { @ExcelProperty("学生编号") private Integer id; @ExcelProperty("学生姓名") private String name; @ExcelProperty("学生薪水") private Double salary; @ExcelProperty("学生生日") private Date birthday; }
@ExcelProperty
就是用来指定表头信息,再次执行之前的测试类方法,如下:当然如果想要自定义列的顺序时,可以修改POJO,如下:
@Data public class Student implements Serializable { @ExcelProperty(value = "学生编号",order = 10) private Integer id; @ExcelProperty(value = "学生姓名",order = 2) private String name; @ExcelProperty(value = "学生薪水",order = 1) private Double salary; @ExcelProperty(value = "学生生日",order = 11) private Date birthday; }
再次执行方法,
order
的值越大,列越往右,如下:
有时候更多的时候需要在表头上,在加上一个表头,例如为学生信息
修改POJO类如下:
@Data
public class Student implements Serializable {
@ExcelProperty(value = {"学生信息","学生编号"},order = 10)
private Integer id;
@ExcelProperty(value = {"学生信息","学生姓名"},order = 2)
private String name;
@ExcelProperty(value = {"学生信息","学生薪水"},order = 1)
private Double salary;
@ExcelProperty(value = {"学生信息","学生生日"},order = 11)
private Date birthday;
}
再去执行之前的测试方式,内容如下:
-
列宽,行高定义
从上述例子可知,之前操作,产生的列的宽度与内容的宽度并没有对应,所以需要手动指定宽度,修改POJO类如下:
@HeadRowHeight(value = 35) // 表头行高 @ContentRowHeight(value = 25) // 内容行高 @ColumnWidth(value = 50) // 列宽 @Data public class Student implements Serializable { @ExcelProperty(value = {"学生信息","学生编号"},order = 10) private Integer id; @ExcelProperty(value = {"学生信息","学生姓名"},order = 2) private String name; @ExcelProperty(value = {"学生信息","学生薪水"},order = 1) private Double salary; @ExcelProperty(value = {"学生信息","学生生日"},order = 11) private Date birthday; }
执行测试方法,内容如下:
虽然上述方式能够修改列宽,但是宽度太大,实际开发中更希望根据内容自适应宽度,步骤如下:
修改POJO类,将列宽注解去掉
修改测试方法,内容如下:
@Test void contextLoads() { EasyExcel.write("学生信息表.xlsx", Student.class) // 自适应宽度,但是这个不是特别精确 .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .sheet() .doWrite(getData()); }
执行测试方法,内容如下:
注意:这个自适应宽度,不是特别精确
-
日期格式化
从上述例子中,发现日期格式都是固定的格式,但是有时候需要自定义格式,因此可以修改POJO类达到以下效果,如下:
执行测试方法,结果如下:
3.1.3 忽略写
实际开发过程中,并不像把所有的属性数据全部写出,那么可以修改POJO类,增加@ExcelIgnore
注解进行忽略,如下:
执行测试方法,如下:
3.1.4 指定写
上述"忽略写"例子中,可以指定哪些属性不输出到Excel
表格中,但是这个种方式是固定的,更多的时候需要动态指定哪些输出,哪些不输出。实现步骤如下:
POJO类:
@HeadRowHeight(value = 35) // 表头行高
@ContentRowHeight(value = 25) // 内容行高
@Data
public class Student implements Serializable {
@ExcelProperty(value = {"学生信息","学生编号"},order = 10)
private Integer id;
@ExcelProperty(value = {"学生信息","学生姓名"},order = 2)
private String name;
@ExcelProperty(value = {"学生信息","学生薪水"},order = 1)
private Double salary;
@ExcelProperty(value = {"学生信息","学生生日"},order = 11)
@DateTimeFormat("yyyy-MM-dd")
private Date birthday;
}
测试方法:
@Test
void contextLoads() {
// 设置 要导出列的属性名
// 必须要跟类型的属性名保持一致
Set set = new HashSet<>();
set.add("id");
set.add("name");
EasyExcel.write("学生信息表.xlsx", Student.class)
.includeColumnFiledNames(set)
// 自适应宽度,但是这个不是特别精确
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.sheet()
.doWrite(getData());
}
执行测试方法,内容如下:
3.2.读操作
3.2.1 简单读
-
简介
读取在实际开发中也占据了较大地位,但是读取并不是读取任意的一个
Excel
文件,而是读取按照事先提供好的Excel
模板,用户在模块版上修改数据的Excel
-
准备POJO类
package com.briup.easyexcel.pojo; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.format.DateTimeFormat; import com.alibaba.excel.annotation.write.style.ContentRowHeight; import com.alibaba.excel.annotation.write.style.HeadRowHeight; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data public class Student_Read implements Serializable { @ExcelProperty(value = {"学生信息","学生编号"}) private Integer id; @ExcelProperty(value = {"学生信息","学生姓名"}) private String name; @ExcelProperty(value = {"学生信息","学生薪水"}) private Double salary; @ExcelProperty(value = {"学生信息","学生生日"}) private Date birthday; }
如上:如果使用该类的对象去装载
Excel
中的数据,那么读取时就只能读取以下样式的Excel
数据,否则数据部分丢失或者全部丢失 -
准备
Excel
文件,内容如下:这里我是放到项目的根路径下
-
在测试类中准备测试方法,用来读取
@Test void readExcel() throws Exception { List
list = new ArrayList<>(); /* * EasyExcel 读取 是基于SAX方式 * 因此在解析时需要传入监听器 */ // 第一个参数 为 excel文件路径 // 读取时的数据类型 // 监听器 EasyExcel.read("学生信息表" + ExcelTypeEnum.XLSX.getValue(), Student_Read.class, new AnalysisEventListener () { // 每读取一行就调用该方法 @Override public void invoke(Student_Read data, AnalysisContext context) { list.add(data); } // 全部读取完成就调用该方法 @Override public void doAfterAllAnalysed(AnalysisContext context) { System.out.println("读取完成"); } }).sheet().doRead(); list.forEach(System.out::println); } -
执行测试方法,结果如下:
以下是对测试方法代码的解释(started)
如上图:
EasyExcel.read
该方法是用来创建ExcelReaderBuilder
对象,该对象就是用来解析Excel文档
read
方法需要传入三个参数,其具体含义如下:
-
第一个参数
需要解析文件的路径,当然除了传入一个文件路径以外,还可以传入
InputStream
源码如下:
-
第二参数
数据类型的Class类型对象,可以不传
-
第三个参数
事件监听器,在之前介绍这款框架时说过,该框架是基于
SAX
的一种解析,加载一行数据到内存就会去解析一行,主要是为了节约内存。invoke
方法代表每解析一行就会调用一次,data数据表示解析出来一行的数据doAfterAllAnalysed
该方法表示将所有数据解析完毕以后才会去调用该方法
sheet
方法代表读取excel
第几个sheet,常用sheet
方法如下:
用法与之前写的用法类似,这里就不再过多介绍
doRead
方法代表开始读取excel
数据
4.2 其他读
实际开发中一般读操作用的最多的就是简单读,如果实在是有复杂的需求,例如读取表头或者是调用Excel
里面的公式,可以参照官方文档
4.WEB
上述例子中,读写操作就是在本地去操作Excel
文档,实际开发中都是在web中,但是其实用法都是一样的,只不过数据的来源不一样,这里提供了一个Excel
工具类,其中就包括web
操作。如下:
package com.briup.server.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.listener.ReadListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.briup.server.exception.SMSException;
import com.briup.server.logging.LogHolder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
public class ExcelUtil {
/**
* 写出一个 excel 文件到本地
*
* 将类型所有加了 @ExcelProperty 注解的属性全部写出
*
* @param fileName 文件名 不要后缀
* @param sheetName sheet名
* @param data 写出的数据
* @param clazz 要写出数据类的Class类型对象
* @param 写出的数据类型
*/
public static void writeExcel(String fileName, String sheetName, List data, Class clazz) {
writeExcel(null, fileName, sheetName, data, clazz);
}
/**
* 按照指定的属性名进行写出 一个 excel
*
* @param attrName 指定的属性名 必须与数据类型的属性名一致
* @param fileName 文件名 不要后缀
* @param sheetName sheet名
* @param data 要写出的数据
* @param clazz 要写出数据类的Class类型对象
* @param 要写出的数据类型
*/
public static void writeExcel(Set attrName, String fileName, String sheetName, List data, Class clazz) {
fileName = StringUtils.isBlank(fileName) ? "学生管理系统" : fileName;
sheetName = StringUtils.isBlank(sheetName) ? "sheet0" : sheetName;
try(FileOutputStream fos = new FileOutputStream(fileName)) {
write(fos,attrName,sheetName,data,clazz);
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* 读取 指定格式的 excel文档
*
* @param fileName 文件名
* @param clazz 数据类型的class对象
* @param 数据类型
* @return
*/
public static List readExcel(String fileName, Class clazz) {
return readExcel(fileName, clazz, null);
}
/**
* 取 指定格式的 excel文档
* 注意一旦传入自定义监听器,则返回的list为空,数据需要在自定义监听器里面获取
*
* @param fileName 文件名
* @param clazz 数据类型的class对象
* @param readListener 自定义监听器
* @param 数据类型
* @return
*/
public static List readExcel(String fileName, Class clazz, ReadListener readListener) {
try(FileInputStream fis = new FileInputStream(fileName)) {
return read(fis,clazz,readListener);
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* 导出 一个 excel
* 导出excel所有数据
* @param response
* @param fileName 件名 最好为英文,不要后缀名
* @param sheetName sheet名
* @param data 要写出的数据
* @param clazz 要写出数据类的Class类型对象
* @param 要写出的数据类型
*/
public static void export(HttpServletResponse response, String fileName, String sheetName, List data, Class clazz) {
export(response, null, fileName, sheetName, data, clazz);
}
/**
* 按照指定的属性名进行写出 一个 excel
*
* @param response
* @param attrName 指定的属性名 必须与数据类型的属性名一致
* @param fileName 文件名 最好为英文,不要后缀名
* @param sheetName sheet名
* @param data 要写出的数据
* @param clazz 要写出数据类的Class类型对象
* @param 要写出的数据类型
*/
public static void export(HttpServletResponse response, Set attrName, String fileName, String sheetName, List data, Class clazz) {
fileName = StringUtils.isBlank(fileName) ? "student-system-manager" : fileName;
sheetName = StringUtils.isBlank(sheetName) ? "sheet0" : sheetName;
response.setContentType("application/vnd.ms-excel;charset=utf-8");
response.setCharacterEncoding("utf-8");
response.addHeader("Content-disposition", "attachment;filename=" + fileName + ExcelTypeEnum.XLSX.getValue());
try(OutputStream os = response.getOutputStream()) {
write(os,attrName,sheetName,data,clazz);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 接收一个excel文件,并且进行解析
* 注意一旦传入自定义监听器,则返回的list为空,数据需要在自定义监听器里面获取
* @param multipartFile excel文件
* @param clazz 数据类型的class对象
* @param readListener 监听器
* @param
* @return
*/
public static List importExcel(MultipartFile multipartFile,Class clazz,ReadListener readListener) {
try(InputStream inputStream = multipartFile.getInputStream()) {
return read(inputStream,clazz,readListener);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void write(OutputStream os, Set attrName, String sheetName, List data, Class clazz) {
ExcelWriterBuilder write = EasyExcel.write(os, clazz);
// 如果没有指定要写出那些属性数据,则写出全部
if (!CollectionUtils.isEmpty(attrName)) {
write.includeColumnFiledNames(attrName);
}
write.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()).sheet(sheetName).doWrite(data);
}
private static List read(InputStream in,Class clazz, ReadListener readListener) {
List list = new ArrayList<>();
Optional optional = Optional.ofNullable(readListener);
EasyExcel.read(in, clazz, optional.orElse(new AnalysisEventListener() {
@Override
public void invoke(T data, AnalysisContext context) {
list.add(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("解析完成");
}
})).sheet().doRead();
return list;
}
}