官方文档:https://easyexcel.opensource.alibaba.com/docs/current/
从Excel读入数据
最简单的读在这里不再赘述,参考文章最上方的官方文档(全中文的,里面已经写得很通俗易懂了)。这里只大致讲一下主要步骤:
1.定义接收数据的对象结构;
2.定义处理数据的监听器,其中包含每批数据解析完的清理机制和储存机制。拿官方文档的demo来举例:
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private List<DemoData> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(DemoData data, AnalysisContext context) {
log.info("解析到一条数据:{}", JSON.toJSONString(data));
cachedDataList.add(data);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (cachedDataList.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
}
}
监听器里定义了BATCH_COUNT,每行解析出来的数据都会放入cachedDataList 中,cachedDataList 达到BATCH_COUNT会先存储这部分数据,然后将cachedDataList清掉。这样保证读是一批一批落库的,不会一下读大量数据OOM。
动态读
编码过程中遇到的实际情况是用户输入的excel是动态表头,我们无法使用特定的对象去接收数据,只能用最基础的Map去接收。参考官方文档中不创建对象的读。
private List<Map<Integer, String>> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
用来接收数据的结构是List<Map<Integer, String>>,其中Map<Integer, String>>是每一行的数据,map的key是列序号,从0开始,value是此行此列单元格的值。
只解析部分数据
解决方案1:
参考了https://blog.csdn.net/u012751272/article/details/126298071的解决方案,感谢大佬。
思路就是解析完需要的数据后用抛出异常来中断后续解析流程,外层捕获异常即可,不影响解析功能,且能大幅提高性能。比如需求只需解析出20行示例数据,程序只需解析完这20行即可返回,无需等待整个表所有数据解析完。
解决方案2:
如果需求要求的是解析示例数据同时返回用户输入的excel的总行数,那就不能采用抛出异常中断流程的方法。代码(已脱敏)如下:
public class ParsePartExcelListener extends AnalysisEventListener<Map<Integer, String>> {
private List<Map<Integer, String>> dataList = new ArrayList<>();
private List<Map<Integer, String>> headList = new ArrayList<>();
//需要解析的行数
private Integer limitSize;
//总行数
private Integer rowCount = 0;
private int BATCH_SIZE = 100;
public ParsePartExcelListener(int limitSize) {
this.limitSize = limitSize;
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
headList.add(headMap);
}
@Override
public void invoke(Map<Integer, String> data, AnalysisContext analysisContext) {
if (!(Objects.nonNull(limitSize) && rowCount >= limitSize)) {
dataList.add(data);
}
rowCount++;
// 达到BATCH_COUNT了,需要去清一次数据,防止数据几万条数据在内存,容易OOM
if (dataList.size() >= BATCH_SIZE) {
// 存储完成清理 list
dataList.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("所有数据解析完成!");
}
}
每批解析出的数据复杂处理
和demo中的存储逻辑一样,在解析完一批次数据之后,统一对这一批数据进行处理,然后清除。
public class ParseExcelListener extends AnalysisEventListener<Map<Integer, String>> {
private Map<Integer, Map<Integer, String>> dataMap = Maps.newHashMap();
private List<Map<Integer, String>> headList = new ArrayList<>();
private ExcelDataHandler<Map<Integer, String>> dataHandler;
private int rowId = 1;
public ParseExcelListener(ExcelDataHandler<Map<Integer, String>> dataHandler) {
this.dataHandler = dataHandler;
}
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
headList.add(headMap);
}
@Override
public void invoke(Map<Integer, String> integerStringMap, AnalysisContext analysisContext) {
dataMap.put(rowId, integerStringMap);
rowId++;
// 达到BATCH_COUNT了,需要去清一次数据,防止数据几万条数据在内存,容易OOM
if (dataMap.size() >= dataHandler.getHandlerBatch()) {
dataHandler.actuator(dataMap, headList);
// 存储完成清理 list
dataMap.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
log.info("所有数据解析完成!");
dataHandler.actuator(dataMap, headList);
}
}
ParseExcelListener parseExcelListener = new ParseExcelListener(new ExcelDataHandler<Map<Integer, String>>() {
/**
* @Param: Map<行号, Map < 列号, 单元格内容>>
*/
@Override
public void actuator(Map<Integer, Map<Integer, String>> dataMap, List<Map<Integer, String>> headList) {
//数据处理
}
});
try {
EasyExcelFactory.read(inputStream).registerReadListener(parseExcelListener).headRowNumber(1).sheet().doRead();
} catch (Exception e) {
log.warn("解析数据异常,文件url:{},错误原因:{}", importContext.getImportsRecord().getOriginUrl(), e);
throw e;
}
边读边写,如何追加写入
读是分批读,每一批有一个批量处理数据的方法,可以在批量处理数据时将读取到的数据处理后写入新文件中。
先看demo:
/**
* 重复多次写入
* <p>
* 1. 创建excel对应的实体对象 参照{@link ComplexHeadData}
* <p>
* 2. 使用{@link ExcelProperty}注解指定复杂的头
* <p>
* 3. 直接调用二次写入即可
*/
@Test
public void repeatedWrite() {
// 方法1: 如果写到同一个sheet
String fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 需要指定写用哪个class去写
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
// 这里注意 如果同一个sheet只要创建一次
WriteSheet writeSheet = EasyExcel.writerSheet("模板").build();
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来
for (int i = 0; i < 5; i++) {
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
}
// 方法2: 如果写到不同的sheet 同一个对象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName, DemoData.class).build()) {
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
}
// 方法3 如果写到不同的sheet 不同的对象
fileName = TestFileUtil.getPath() + "repeatedWrite" + System.currentTimeMillis() + ".xlsx";
// 这里 指定文件
try (ExcelWriter excelWriter = EasyExcel.write(fileName).build()) {
// 去调用写入,这里我调用了五次,实际使用时根据数据库分页的总的页数来。这里最终会写到5个sheet里面
for (int i = 0; i < 5; i++) {
// 每次都要创建writeSheet 这里注意必须指定sheetNo 而且sheetName必须不一样。这里注意DemoData.class 可以每次都变,我这里为了方便 所以用的同一个class
// 实际上可以一直变
WriteSheet writeSheet = EasyExcel.writerSheet(i, "模板" + i).head(DemoData.class).build();
// 分页去数据库查询数据 这里可以去数据库查询每一页的数据
List<DemoData> data = data();
excelWriter.write(data, writeSheet);
}
}
}
//本地新建一个临时文件
File file = FileUtil.create(filePath, fileName);
ExcelWriter excelWriter = EasyExcelFactory.write(file).build();
WriteSheet writeSheet = EasyExcelFactory.writerSheet().build();
ParseExcelListener parseExcelListener = new ParseExcelListener(new ExcelDataHandler<Map<Integer, String>>() {
/**
* @Param: Map<行号, Map < 列号, 单元格内容>>
*/
@Override
public void actuator(Map<Integer, Map<Integer, String>> dataMap, List<Map<Integer, String>> headList) {
//每批数据统一处理
//判断临时文件是否已存在,已存在则不需要表头,直接往后面追加写
if (!file.exists() || file.length() == 0) {
writeSheet.setHead(getHeader(importContext.getMapping(), excelHeadIdxNameMap));
}
excelWriter.write(importSafetyInventoryResults, writeSheet);
}
});
try {
EasyExcelFactory.read(inputStream).registerReadListener(parseExcelListener).headRowNumber(1).sheet().doRead();
} catch (Exception e) {
log.warn("解析数据异常,文件url:{},错误原因:{}", importContext.getImportsRecord().getOriginUrl(), e);
throw e;
} finally {
//所有数据解析完再关闭excelWriter,否则每批数据都会覆盖前面的内容
if (excelWriter != null) {
excelWriter.finish();
}
}
//处理完需要把本地的临时文件删掉
FileUtil.clearFile(file);
鸣谢
https://easyexcel.opensource.alibaba.com/docs/current/quickstart/read#%E4%B8%8D%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1%E7%9A%84%E8%AF%BB
https://blog.csdn.net/u012751272/article/details/126298071