1. Excel解析工具easyexcel全面探索
1.1. 简介
之前我们想到Excel解析一般是使用POI,但POI存在一个严重的问题,就是非常消耗内存。所以阿里人员对它进行了重写从而诞生了easyexcel,它解决了过于消耗内存问题,也对它进行了封装让使用者使用更加便利
接下来我先一一介绍它所有的功能细节、如何使用及部分源码解析
1.2. Excel读
1.2.1. 例子
/**
* 最简单的读
*
1. 创建excel对应的实体对象 参照{@link DemoData}
*
2. 由于默认异步读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
*
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
1.2.2.2. doRead
- 例子中真正开始做解析任务的是doRead方法,不断进入此方法,会看到真正执行的最后方法就是XlsxSaxAnalyser类的execute方法;可以看到如下方法中parseXmlSource解析的就是sheetMap缓存的真正数据
@Override
public void execute(List
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. 例子
- 如下例子,使用还是简单的,和读比较类似
/**
* 最简单的写
*
1. 创建excel对应的实体对象 参照{@link com.alibaba.easyexcel.test.demo.write.DemoData}
*
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
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就是添加数据了,由excelBuilderExcel建造者来添加,这是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 == null) {
excelWriteAddExecutor = new ExcelWriteAddExecutor(context);
}
//核心
excelWriteAddExecutor.add(data);
} catch (RuntimeException e) {
finish();
throw e;
} catch (Throwable e) {
finish();
throw new ExcelGenerateException(e);
}
}
1.3.2.4. add
- 可以看到很明显在遍历数据addOneRowOfDataToExcel插入到Excel表了
public void add(List data) {
if (CollectionUtils.isEmpty(data)) {
return;
}
WriteSheetHolder writeSheetHolder = writeContext.writeSheetHolder();
int newRowIndex = writeSheetHolder.getNewRowIndexAndStartDoWrite();
if (writeSheetHolder.isNew() && !writeSheetHolder.getExcelWriteHeadProperty().hasHead()) {
newRowIndex += writeContext.currentWriteHolder().relativeHeadRowIndex();
}
// BeanMap is out of order,so use fieldList
List
1.3.2.5. addOneRowOfDataToExcel
- 这里先是做创建Excel行的准备,包括行的一些属性处理器需不需要处理,之后我们的例子是插入java对象,进入addJavaObjectToExcel方法
private void addOneRowOfDataToExcel(Object oneRowData, int n, int relativeRowIndex, List
1.3.2.6. addJavaObjectToExcel
- 在ExcelWriteAddExecutor执行器类中执行addJavaObjectToExcel,在这里进行了数据的解析,将数据解析成标题和内容,封装成适合Excel的格式CellData,数据类型等,经过这步我们还没看到文件流的生成,那么下一步了
private void addJavaObjectToExcel(Object oneRowData, Row row, int relativeRowIndex, List
1.3.2.7. finish
- doWrite中之后还有一步finish
public void finish() {
excelBuilder.finish();
}
- 深入ExcelBuilderImpl类
@Override
public void finish() {
if (context != null) {
context.finish();
}
}
- 到WriteContextImpl写内容实现类的finish方法中,我们可以看到writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream()); 这句是重点,将写Excel持有容器中的内容流输出;之后就是关闭流,删除临时文件的过程
@Override
public void finish() {
WriteHandlerUtils.afterWorkbookDispose(this);
if (writeWorkbookHolder == null) {
return;
}
Throwable throwable = null;
boolean isOutputStreamEncrypt = false;
try {
isOutputStreamEncrypt = doOutputStreamEncrypt07();
} catch (Throwable t) {
throwable = t;
}
if (!isOutputStreamEncrypt) {
try {
// 重点
writeWorkbookHolder.getWorkbook().write(writeWorkbookHolder.getOutputStream());
writeWorkbookHolder.getWorkbook().close();
} catch (Throwable t) {
throwable = t;
}
}
try {
Workbook workbook = writeWorkbookHolder.getWorkbook();
if (workbook instanceof SXSSFWorkbook) {
((SXSSFWorkbook)workbook).dispose();
}
} catch (Throwable t) {
throwable = t;
}
try {
if (writeWorkbookHolder.getAutoCloseStream() && writeWorkbookHolder.getOutputStream() != null) {
writeWorkbookHolder.getOutputStream().close();
}
} catch (Throwable t) {
throwable = t;
}
if (!isOutputStreamEncrypt) {
try {
doFileEncrypt07();
} catch (Throwable t) {
throwable = t;
}
}
try {
if (writeWorkbookHolder.getTempTemplateInputStream() != null) {
writeWorkbookHolder.getTempTemplateInputStream().close();
}
} catch (Throwable t) {
throwable = t;
}
clearEncrypt03();
if (throwable != null) {
throw new ExcelGenerateException("Can not close IO", throwable);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Finished write.");
}
}
1.4. 文件上传
- 它提供了一个接收InputStream的参数,之后和Excel读没多大区别
/**
* 文件上传
*
* 1. 创建excel对应的实体对象 参照{@link UploadData}
*
* 2. 由于默认异步读取excel,所以需要创建excel一行一行的回调监听器,参照{@link UploadDataListener}
*
* 3. 直接读即可
*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), UploadData.class, new UploadDataListener()).sheet().doRead();
return "success";
}
1.5. 文件下载
- 写入提供参数OutputStream,其它和文件写入差不多
/**
* 文件下载
*
* 1. 创建excel对应的实体对象 参照{@link DownloadData}
*
* 2. 设置返回的 参数
*
* 3. 直接写,这里注意,finish的时候会自动关闭OutputStream,当然你外面再关闭流问题不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 这里URLEncoder.encode可以防止中文乱码 当然和easyexcel没有关系
String fileName = URLEncoder.encode("测试", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), DownloadData.class).sheet("模板").doWrite(data());
}
1.6. 读取技巧
1.6.1. Excel读取多页
- 以上都是最基础的单页读写,在我们调用sheet()方法时,实际上都是默认第1页,那么如何读取多页?
/**
* 读多个或者全部sheet,这里注意一个sheet不能读取多次,多次读取需要重新读取文件
*
* 1. 创建excel对应的实体对象 参照{@link DemoData}
*
* 2. 由于默认异步读取excel,所以需要创建excel一行一行的回调监听器,参照{@link DemoDataListener}
*
* 3. 直接读即可
*/
@Test
public void repeatedRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 读取全部sheet
// 这里需要注意 DemoDataListener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个DemoDataListener里面写
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).doReadAll();
// 读取部分sheet
fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
ExcelReader excelReader = EasyExcel.read(fileName).build();
// 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
ReadSheet readSheet1 =
EasyExcel.readSheet(0).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
ReadSheet readSheet2 =
EasyExcel.readSheet(1).head(DemoData.class).registerReadListener(new DemoDataListener()).build();
// 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
excelReader.read(readSheet1, readSheet2);
// 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
excelReader.finish();
}
- 可以看到doReadAll方法可以读取所有sheet页面
- 若要读取单独的页面,用第二种方式readSheet(index),index为页面位置,从0开始计数
1.6.2. 自定义字段转换
- 在读取写入的时候,我们可能会有这样的需求:比如日期格式转换,字符串添加固定前缀后缀等等,此时我们可以进行自定义编写
@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;
}
- 如上面的CustomStringStringConverter类为自定义转换器,可以对字符串进行一定修改,而日期数字的格式化,它已经有提供注解了DateTimeFormat和NumberFormat
- 转换器如下,实现Converter接口后即可使用supportExcelTypeKey这是判断单元格类型,convertToJavaData这是读取转换,convertToExcelData这是写入转换
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;
public class CustomStringStringConverter implements Converter
- 这里解析结果截取部分如下,原数据是字符串0 2020/1/1 1:01 1
解析到一条数据:{"date":"2020年01月01日01时01分01秒","doubleData":"100%","string":"自定义:字符串0"}
1.6.3. 指定表头行数
EasyExcel.read(fileName, DemoData.class, new DemoDataListener()).sheet()
// 这里可以设置1,因为头就是一行。如果多行头,可以设置其他值。不传入也可以,因为默认会根据DemoData 来解析,他没有指定头,也就是默认1行
.headRowNumber(1).doRead();
1.6.4. 读取表头数据
- 只要在实现了AnalysisEventListener接口的监听器中,重写invokeHeadMap方法即可
/**
* 这里会一行行的返回头
*
* @param headMap
* @param context
*/
@Override
public void invokeHeadMap(Map
1.6.5. 转换异常处理
- 只要在实现了AnalysisEventListener接口的监听器中,重写onException方法即可
@Override
public void onException(Exception exception, AnalysisContext context) {
LOGGER.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
LOGGER.error("第{}行,第{}列解析异常", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex());
}
}
1.6.6. 读取单元格参数和类型
- 将类属性用CellData封装起来
@Data
public class CellDataReadDemoData {
private CellData
- 这样读取到的数据如下,会包含单元格数据类型
解析到一条数据:{"date":{"data":1577811661000,"dataFormat":22,"dataFormatString":"m/d/yy h:mm","formula":false,"numberValue":43831.0423726852,"type":"NUMBER"},"doubleData":{"data":1.0,"formula":false,"numberValue":1,"type":"NUMBER"},"formulaValue":{"data":"字符串01","formula":true,"formulaValue":"_xlfn.CONCAT(A2,C2)","stringValue":"字符串01","type":"STRING"},"string":{"data":"字符串0","dataFormat":0,"dataFormatString":"General","formula":false,"stringValue":"字符串0","type":"STRING"}}
1.6.7. 同步返回
- 不推荐使用,但如果特定情况一定要用,可以如下,主要为doReadSync方法,直接返回List
/**
* 同步的返回,不推荐使用,如果数据量大会把数据放到内存里面
*/
@Test
public void synchronousRead() {
String fileName = TestFileUtil.getPath() + "demo" + File.separator + "demo.xlsx";
// 这里 需要指定读用哪个class去读,然后读取第一个sheet 同步读取会自动finish
List