1. Excel解析工具easyexcel全面探索
1.1. 简介
之前我们想到Excel解析一般是使用POI,但POI存在一个严重的问题,就是非常消耗内存。所以阿里人员对它进行了重写从而诞生了easyexcel
,它解决了过于消耗内存问题,也对它进行了封装让使用者使用更加便利
接下来我先一一介绍它所有的功能细节、如何使用及部分源码解析
1.2. Excel读
1.2.1. 例子
/**
* 最简单的读
* <p>1. 创建excel对应的实体对象 参照{@link DemoData}
* <p>2. 由于默认异步读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
* <p>3. 直接读即可
*/
@Test
public void simpleRead() {
String fileName = TestFileUtil.getPath() "demo" File.separator "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet().doRead();
}
- 官方说明也比较明确,使用简单
fileName
是路径 文件名
,DemoData
是Excel数据对应的实体类,DemoDataListener
这看名字就是监听器,用来监听处理读取到的每一条数据
1.2.2. 源码解析
1.2.2.1. 核心源码XlsxSaxAnalyser
- 它核心的Excel解析我认为是这个类
XlsxSaxAnalyser
,在它的构造方法中做了很多事
public XlsxSaxAnalyser(AnalysisContext analysisContext, InputStream decryptedStream) throws Exception {
...
//从这开始将数据读取成inputStream流,缓存到了sheetMap
XSSFReader xssfReader = new XSSFReader(pkg);
analysisUse1904WindowDate(xssfReader, readWorkbookHolder);
stylesTable = xssfReader.getStylesTable();
sheetList = new ArrayList<ReadSheet>();
sheetMap = new HashMap<Integer, InputStream>();
XSSFReader.SheetIterator ite = (XSSFReader.SheetIterator)xssfReader.getSheetsData();
int index = 0;
if (!ite.hasNext()) {
throw new ExcelAnalysisException("Can not find any sheet!");
}
while (ite.hasNext()) {
InputStream inputStream = ite.next();
sheetList.add(new ReadSheet(index, ite.getSheetName()));
sheetMap.put(index, inputStream);
index ;
}
}
1.2.2.2. doRead
- 例子中真正开始做解析任务的是
doRead
方法,不断进入此方法,会看到真正执行的最后方法就是XlsxSaxAnalyser
类的execute
方法;可以看到如下方法中parseXmlSource
解析的就是sheetMap
缓存的真正数据
@Override
public void execute(List<ReadSheet> readSheetList, Boolean readAll) {
for (ReadSheet readSheet : sheetList) {
readSheet = SheetUtils.match(readSheet, readSheetList, readAll,
analysisContext.readWorkbookHolder().getGlobalConfiguration());
if (readSheet != null) {
analysisContext.currentSheet(readSheet);
parseXmlSource(sheetMap.get(readSheet.getSheetNo()), new XlsxRowHandler(analysisContext, stylesTable));
// The last sheet is read
analysisContext.readSheetHolder().notifyAfterAllAnalysed(analysisContext);
}
}
}
1.2.2.3. 概述DemoDataListener
实现
- 对应我们用户需要手写的代码,我们的监听器
DemoDataListener
中有两个实现方法如下,invoke
就对应了上述代码中的parseXmlSource
而doAfterAllAnalysed
对应了上述方法中的notifyAfterAllAnalysed
,分别表示了先解析每一条数据和当最后一页读取完毕通知所有监听器
@Override
public void invoke(DemoData data, AnalysisContext context) {
LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data));
list.add(data);
if (list.size() >= BATCH_COUNT) {
saveData();
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
saveData();
LOGGER.info("所有数据解析完成!");
}
1.2.2.4. parseXmlSource
具体实现
- 看标识重点的地方,这是最核心的解析地
private void parseXmlSource(InputStream inputStream, ContentHandler handler) {
InputSource inputSource = new InputSource(inputStream);
try {
SAXParserFactory saxFactory = SAXParserFactory.newInstance();
saxFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
SAXParser saxParser = saxFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.setContentHandler(handler);
//重点
xmlReader.parse(inputSource);
inputStream.close();
} catch (ExcelAnalysisException e) {
throw e;
} catch (Exception e) {
throw new ExcelAnalysisException(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
throw new ExcelAnalysisException("Can not close 'inputStream'!");
}
}
}
}
- 由于这层层深入非常多,我用一张截图来表现它的调用形式
1.2.2.5. notifyAfterAllAnalysed
具体实现
- 具体看
notifyAfterAllAnalysed
的代码,我们实现的DemoDataListener
监听器继承AnalysisEventListener
,而AnalysisEventListener
实现ReadListener
接口
@Override
public void notifyAfterAllAnalysed(AnalysisContext analysisContext) {
for (ReadListener readListener : readListenerList) {
readListener.doAfterAllAnalysed(analysisContext);
}
}
1.3. Excel写
1.3.1. 例子
- 如下例子,使用还是简单的,和读比较类似
/**
* 最简单的写
* <p>1. 创建excel对应的实体对象 参照{@link com.alibaba.easyexcel.test.demo.write.DemoData}
* <p>2. 直接写即可
*/
@Test
public void simpleWrite() {
String fileName = TestFileUtil.getPath() "write" System.currentTimeMillis() ".xlsx";
// 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(fileName, DemoData.class).sheet("模板").doWrite(data());
}
private List<DemoData> data() {
List<DemoData> list = new ArrayList<DemoData>();
for (int i = 0; i < 10; i ) {
DemoData data = new DemoData();
data.setString("字符串" i);
data.setDate(new Date());
data.setDoubleData(0.56);
list.add(data);
}
return list;
}
1.3.2. 源码解析
1.3.2.1. doWrite
- 和读一样
doWrite
才是实际做事的,这次我们从这个入口跟进
public void doWrite(List data) {
if (excelWriter == null) {
throw new ExcelGenerateException("Must use 'EasyExcelFactory.write().sheet()' to call this method");
}
excelWriter.write(data, build());
excelWriter.finish();
}
1.3.2.2. write
- 很明显,
write
是核心,继续进入ExcelWriter
类,看名字addContent
就是添加数据了,由excelBuilder
Excel建造者来添加,这是ExcelBuilderImpl
类
public ExcelWriter write(List data, WriteSheet writeSheet, WriteTable writeTable) {
excelBuilder.addContent(data, writeSheet, writeTable);
return this;
}
1.3.2.3. addContent
- 可以看到如下,显示封装和实例化一些数据,创建了
ExcelWriteAddExecutor
写数据执行器,核心就是add
方法了
@Override
public void addContent(List data, WriteSheet writeSheet, WriteTable writeTable) {
try {
if (data == null) {
return;
}
context.currentSheet(writeSheet, WriteTypeEnum.ADD);
context.currentTable(writeTable);
if (excelWriteAddExecutor =