针对于后端开发者而言的,作为报表的导入和导出是一个很基础且有很棘手的问题!之前常用的工具和方案大概有这么几种:
因为当数据量特别大的时候,比如:说Excel导出,如果数据量在百万级,很有可能会出现俩点内存溢出的问题以及页面极具卡顿。
首先,在百万级大数据量Excel文件的导出我们可以采用分批查询数据来避免内存溢出的剧增!
此外,POI给出的方案是:使用SXSSFWorkbook方式缓存数据到文件上以解决下载大文件EXCEL卡死页面的问题。
对此阿里巴巴发明咯一个“万金油”!EasyExcel(简单Excel操作工具,让Excel变得更简单!),除此之外它可以将解析的Excel的内存占用控制在KB级别,并且绝对不会内存溢出,还有就是速度极快, 不用想了就它了!
EasyExcel是一个基于Java实现的、以节省内存为主要目标的的读写Excel文件的开源项目。经过官方统计,在尽可能节约内存的情况下支持读写百M的Excel文件的读写操作能力!
当利用POI去读取Excel时,首先会将数据全部加载到内存中,然后返回给调用者,当数据量比较大时,及其容易发生OOM。以下是执行流程图:
与POI 不用的是,EasyExcel主要是采用sax模式一行一行解析,并将一行的解析结果以观察者的模式通知处理,即使数据量较大时也不会发生OOM,以下是其执行流程图
借用官方图:
借用官方图片:https://alibaba-easyexcel.github.io/images/large.png
64M内存1分钟内读取75M(46W行25列)的ExcelExcel 内存开销图。当然还有急速模式能更快,但是内存占用会在100M多一点。其实就是拿空间换时间!
我用的版本是2.2.6
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.6</version>
</dependency>
注: 系统内部如果有POI的包一定让poi和poi-ooxml的版本要保持一致。
注: 如果是springboot2.0,则不需要poi依赖,如果是1.0,则需要poi依赖,并且poi和poi-ooxml的版本要保持一致。
<!-- poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
public void testReadEntity() {
// 被读取的文件绝对路径
String fileName = "path/testDemo.xlsx";
// 接收解析出的目标对象(Entity)指的是你的实体类
List<Entity> entityList = new ArrayList<>();
// 这里需要指定读用哪个class去读,然后读取第一个sheet文件流会自动关闭
// excel中表的列要与对象的字段相对应
EasyExcel.read(fileName, Entity.class, new AnalysisEventListener<Student>() {
// 每解析一条数据都会调用该方法
@Override
public void invoke(Entity entity, AnalysisContext analysisContext) {
System.out.println("解析一条Row行对象:" + JSON.toJSONString(entity));
entityList.add(student);
}
// 解析完毕的回调方法
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("excel文件读取完毕!");
}
}).sheet().doRead();
}
// 通过Map作为整体的数据结构模型
public void readWithoutObj() {
// 被读取的文件绝对路径
String fileName = "path/testDemo.xlsx";
// 接收结果集,为一个List列表,每个元素为一个map对象,key-value对为excel中每个列对应的值
List<Map<Integer,String>> resultList = new ArrayList<>();
EasyExcel.read(fileName, new AnalysisEventListener<Map<Integer,String>>() {
@Override
public void invoke(Map<Integer, String> map, AnalysisContext analysisContext) {
System.out.println("解析到一条数据:" + JSON.toJSONString(map));
resultList.add(map);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
System.out.println("excel文件解析完毕!" + JSON.toJSONString(resultList));
}
}).sheet().doRead();
}
invoke方法代表每解析一行就会调用一次,data数据表示解析出来一行的数据。
doAfterAllAnalysed 该方法表示将所有数据解析完毕以后才会去调用该方法。
内部实现机制:(可以理解成一个excel对象,一个excel只要构建一个)
ExcelReaderBuilder 构建出一个 ReadWorkbook
ExcelWriterBuilder 构建出一个 WriteWorkbook
WriteTable(就把excel的一个Sheet,一块区域看一个table)参数
内部实现机制:(可以理解成excel里面的一页,每一页都要构建一个)
内部实现机制:(可以理解成excel里面的一页,每一页都要构建一个)
所有配置都是继承的,Workbook的配置会被Sheet继承,所以在用EasyExcel设置参数的时候,在EasyExcel…sheet()方法之前作用域是整个sheet,之后针对单个sheet。
@Data
public class TestEntity {
/**
* 从0开始,2代表强制读取第三个,
* 不建议 index 和 name 同时用
*/
@ExcelProperty(index = 2)
private Double doubleData;
/**
* 用名字去匹配,这里需要注意,
* 名字重复,会导致只有一个字段读取到数据
*/
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
}
@Data
public class MultiHeaderEntity implements Serializable {
@ExcelProperty(value = {"一层信息","二层1"})
private Integer id;
@ExcelProperty(value = {"一层信息","二层2"})
private String name;
@ExcelProperty(value = {"一层信息","二层4"})
private String description;
@ExcelProperty(value = {"一层信息","二层3"})
private Date birthday;
}
@Data
public class ConverterData {
/**
* 我自定义 转换器,不管数据库传过来什么 。我给他加上“自定义:”
*/
@ExcelProperty(converter = CustomStringStringConverter.class)
private String string;
/**
* 这里用string 去接日期才能格式化。我想接收年月日格式
*/
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
private String date;
/**
* 我想接收百分比的数字
*/
@NumberFormat("#.##%")
private String doubleData;
}
注意 :如果使用该类的对象去装载Excel中的数据,那么读取时就只能读取以下样式的Excel数据模型,否则数据部分丢失或者全部丢失
@Data
public class DemoModel {
private String attribute1;
private Date attribute2;
private Double attribute3;
}
注意:DemoModelAnalysisListener 不要是单例模式或者全局共享,要每次读取excel都要new,否则会出现复用之前读取的过程数据!
@Slf4j
public class DemoModelAnalysisListener extends AnalysisEventListener<DemoData> {
/**
* 每隔500条存储数据库,然后清理list ,方便内存回收
*/
static final int BATCH_COUNT = 500;
List<DemoModel> list = new ArrayList<>();
/**
* 这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private DemoService demoService;
public DemoModelAnalysisListener(DemoService demoService) {
this.demoService = demoService;
}
/**
* 这个每一条数据解析都会来调用
* @param data
* one row value. Is is same as {@link AnalysisContext#readRowHolder()}
* @param context
*/
@Override
public void invoke(DemoData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
list.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,
// 防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
}
}
/**
* 所有数据解析完成了 都会来调用
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
log.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
log.info("{}条数据,开始存储数据库!", list.size());
demoService.insert(list);
// 存储完成清理 list
list.clear();//解析结束销毁不用的资源
log.info("存储数据库成功!");
}
}
@Test
public void readExcelTest() {
// 写法1:
DemoService demoService = getService(); //获取service业务实现类
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new DemoModelAnalysisListener(demoService)).sheet().doRead();
// 写法2:
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
ExcelReader excelReader = null;
try {
excelReader = EasyExcel.read(fileName, DemoData.class, new DemoModelAnalysisListener(demoService)).build();
ReadSheet readSheet = EasyExcel.readSheet(0).build();
excelReader.read(readSheet);
} finally {
if (excelReader != null) {
// 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.finish();
}
}
}
@Data
public class DemoData {
@ExcelProperty("字符串标题")
private String string;
@ExcelProperty("日期标题")
private Date date;
@ExcelProperty("数字标题")
private Double doubleData;
/**
* 忽略这个字段
*/
@ExcelIgnore
private String ignore;
}
@Test
public void demoWriteTest() {
// 写法1
String fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
// 写法2
fileName = TestFileUtil.getPath() + "simpleWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(fileName, DemoData.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
excelWriter.write(data(), writeSheet);
} finally {
// 千万别忘记finish 会帮忙关闭流
if (excelWriter != null) {
excelWriter.finish();
}
}
}
@Data
public class MultiHeaderEntity implements Serializable {
@ExcelProperty(value = {"一层信息","二层1"})
private Integer id;
@ExcelProperty(value = {"一层信息","二层2"})
private String name;
@ExcelProperty(value = {"一层信息","二层4"})
private String description;
@ExcelProperty(value = {"一层信息","二层3"})
private Date birthday;
}
@Test
public void complexHeadWrite() {
String fileName = TestFileUtil.getPath() + "complexHeadWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, MultiHeaderEntity.class).sheet("复杂").doWrite(datalist());
}