今天工作上要求做一个excel的上传和导出,然后查了下说easyexcel这个轮子好像挺好用的,于是大概研究了下,做个笔记,简单记录下。
使用方式很简单,基本就是调调API就能很好的生成excel和读取excel的信息,使用方式如下:
//写
EasyExcel.write("test.xlsx",Domain.class)
.sheet("test")
.doWrite(init());
//读
EasyExcel.read("test.xlsx", MyDomain.class, new MyDemoListener())
.sheet("test")
.doRead();
如果是生成到本地就直接输入路径,构建文件即可,如果是需要web下载和上传就通过流去操作。
整体使用和很简单,用EasyExcel类调用读或者写方法,传入文件名或者流,一个head映射,有两种方式,list>或者class<>,推荐使用后者。class类里面可以给每个字段标注对应的header名字,如果没标注就是字段名,还可以支持设置宽度,内容样式等。详情可以查看官方文档,这里不做赘述。
相比于写,读的时候需要多传入一个ReaderListener,实现如下:
public class MyDemoListener implements ReadListener<MyDomain> {
private List<MyDomain> domains = new ArrayList<>();
private MyDomain domain;
public MyDemoListener() {
this.domain = new MyDomain();
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
}
@Override
public void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context) {
System.out.println("headMap:"+ headMap);
}
@Override
public void invoke(MyDomain data, AnalysisContext context) {
domains.add(data);
}
@Override
public void extra(CellExtra extra, AnalysisContext context) {
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
System.out.println("domains总条数:" + domains.size());
for (int i = 0; i <domains.size() ; i++) {
System.out.println("domain" + domains.get(i));
}
}
@Override
public boolean hasNext(AnalysisContext context) {
return true;
}
}
invokeHead
方法读取标题,里面实现在读完标题后会回调。
写的时候我一直很好奇,invoke
方法读取每一行都会被回调,什么时候会回调呢?入口在XlsxRowHandler类的startElement方法里,EasyExcel采用的是SAX方式解析excel,在遇到开始结束和内容时分别会进入到传入Handler的startElement,endElement和characters方法,整个解析过程比较复杂,会对各种标签就行判断,感兴趣的朋友可以自己研究下,关键代码就是XlsxSaxAnalyser类的execute方法:
public void execute() {
for (ReadSheet readSheet : sheetList) {
//定位读哪些sheet
readSheet = SheetUtils.match(readSheet, xlsxReadContext);
if (readSheet != null) {
xlsxReadContext.currentSheet(readSheet);
parseXmlSource(sheetMap.get(readSheet.getSheetNo()), new XlsxRowHandler(xlsxReadContext));
// Read comments
//读取注解信息
readComments(readSheet);
// The last sheet is read
xlsxReadContext.analysisEventProcessor().endSheet(xlsxReadContext);
}
}
}
然后在XlsxRowHandler这个handler里持有了一个集合map,在调用开始结束和内容那三个方法时会根据name拿到对应的handler,最后再去该handler的startElement方法里面:
@Override
public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
XlsxTagHandler handler = XLSX_CELL_HANDLER_MAP.get(name);
if (handler == null || !handler.support(xlsxReadContext)) {
return;
}
xlsxReadContext.xlsxReadSheetHolder().getTagDeque().push(name);
handler.startElement(xlsxReadContext, name, attributes);
}
以RowTagHandler为例,在对应的endElement方法里调用endRow会进到ReadListenerinvoke方法,
public class RowTagHandler extends AbstractXlsxTagHandler {
、、、、、、、、、、、、、、、、、省略不重要内容、、、、、、、、、、、、、、、、、、、、、、、、、
@Override
public void endElement(XlsxReadContext xlsxReadContext, String name) {
XlsxReadSheetHolder xlsxReadSheetHolder = xlsxReadContext.xlsxReadSheetHolder();
RowTypeEnum rowType = MapUtils.isEmpty(xlsxReadSheetHolder.getCellMap()) ? RowTypeEnum.EMPTY : RowTypeEnum.DATA;
// It's possible that all of the cells in the row are empty
if (rowType == RowTypeEnum.DATA) {
boolean hasData = false;
for (Cell cell : xlsxReadSheetHolder.getCellMap().values()) {
if (!(cell instanceof ReadCellData)) {
hasData = true;
break;
}
ReadCellData<?> readCellData = (ReadCellData<?>)cell;
if (readCellData.getType() != CellDataTypeEnum.EMPTY) {
hasData = true;
break;
}
}
if (!hasData) {
rowType = RowTypeEnum.EMPTY;
}
}
xlsxReadContext.readRowHolder(new ReadRowHolder(xlsxReadSheetHolder.getRowIndex(), rowType,
xlsxReadSheetHolder.getGlobalConfiguration(), xlsxReadSheetHolder.getCellMap()));
//从这里进入到读监听的回调
xlsxReadContext.analysisEventProcessor().endRow(xlsxReadContext);
xlsxReadSheetHolder.setColumnIndex(null);
xlsxReadSheetHolder.setCellMap(new LinkedHashMap<>());
}
}
具体实现在DefaultAnalysisEventProcessor的dealData里,回调所有Listener的invoke和invokeMap方法。
private void dealData(AnalysisContext analysisContext) {
ReadRowHolder readRowHolder = analysisContext.readRowHolder();
Map<Integer, ReadCellData<?>> cellDataMap = (Map)readRowHolder.getCellMap();
readRowHolder.setCurrentRowAnalysisResult(cellDataMap);
int rowIndex = readRowHolder.getRowIndex();
int currentHeadRowNumber = analysisContext.readSheetHolder().getHeadRowNumber();
boolean isData = rowIndex >= currentHeadRowNumber;
// Last head column
if (!isData && currentHeadRowNumber == rowIndex + 1) {
buildHead(analysisContext, cellDataMap);
}
// Now is data
for (ReadListener readListener : analysisContext.currentReadHolder().readListenerList()) {
try {
if (isData) {
readListener.invoke(readRowHolder.getCurrentRowAnalysisResult(), analysisContext);
} else {
readListener.invokeHead(cellDataMap, analysisContext);
}
} catch (Exception e) {
onException(analysisContext, e);
break;
}
if (!readListener.hasNext(analysisContext)) {
throw new ExcelAnalysisStopException();
}
}
}
可以看到监听里实现的三个方法都是在这里回调的,invoke,invokeHead,onException。
还有一个比较好奇的点就是我们在实体类里可以写很多注解,然后用的时候会很方便,但是,它是什么时候被读到的呢?
很简单,源码之下没秘密,带着这个问题来看源码,就会发现整个设计思路变得清晰很多。
以读excel为例,在调用doRead()方法时,会进到excelReader.read(build())
里,在一层层往下走,会最终进到XlsxSaxAnalyser
的execute()
方法里,是不是很熟悉?这里也是上面说的excel解析的地方,感觉核心逻辑都在这块。
注意xlsxReadContext.currentSheet(readSheet);
这行代码,我一开始也被蒙了,以为只是简单的设置一个readSheet,其实不然,里面做了很多初始化的工作,就比如下面这段
public void currentSheet(ReadSheet readSheet) {
switch (readWorkbookHolder.getExcelType()) {
case XLS:
readSheetHolder = new XlsReadSheetHolder(readSheet, readWorkbookHolder);
break;
case XLSX:
readSheetHolder = new XlsxReadSheetHolder(readSheet, readWorkbookHolder);
break;
default:
break;
}
currentReadHolder = readSheetHolder;
if (readWorkbookHolder.getHasReadSheet().contains(readSheetHolder.getSheetNo())) {
throw new ExcelAnalysisException("Cannot read sheet repeatedly.");
}
readWorkbookHolder.getHasReadSheet().add(readSheetHolder.getSheetNo());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Began to read:{}", readSheetHolder);
}
}
在new XlsxReadSheetHolder时,会调用父类的构造方法super(readSheet, readWorkbookHolder);
,然后在继续向上,后面会到AbstractReadHolder的构造函数。
public AbstractReadHolder(ReadBasicParameter readBasicParameter, AbstractReadHolder parentAbstractReadHolder,
Boolean convertAllFiled) {
super(readBasicParameter, parentAbstractReadHolder);
if (readBasicParameter.getUse1904windowing() == null && parentAbstractReadHolder != null) {
getGlobalConfiguration()
.setUse1904windowing(parentAbstractReadHolder.getGlobalConfiguration().getUse1904windowing());
} else {
getGlobalConfiguration().setUse1904windowing(readBasicParameter.getUse1904windowing());
}
if (readBasicParameter.getUseScientificFormat() == null) {
if (parentAbstractReadHolder == null) {
getGlobalConfiguration().setUseScientificFormat(Boolean.FALSE);
} else {
getGlobalConfiguration()
.setUseScientificFormat(parentAbstractReadHolder.getGlobalConfiguration().getUseScientificFormat());
}
} else {
getGlobalConfiguration().setUseScientificFormat(readBasicParameter.getUseScientificFormat());
}
// Initialization property
this.excelReadHeadProperty = new ExcelReadHeadProperty(this, getClazz(), getHead(), convertAllFiled);
if (readBasicParameter.getHeadRowNumber() == null) {
if (parentAbstractReadHolder == null) {
if (excelReadHeadProperty.hasHead()) {
this.headRowNumber = excelReadHeadProperty.getHeadRowNumber();
} else {
this.headRowNumber = 1;
}
} else {
this.headRowNumber = parentAbstractReadHolder.getHeadRowNumber();
}
} else {
this.headRowNumber = readBasicParameter.getHeadRowNumber();
}
if (parentAbstractReadHolder == null) {
this.readListenerList = new ArrayList<ReadListener>();
} else {
this.readListenerList = new ArrayList<ReadListener>(parentAbstractReadHolder.getReadListenerList());
}
if (HolderEnum.WORKBOOK.equals(holderType())) {
Boolean useDefaultListener = ((ReadWorkbook)readBasicParameter).getUseDefaultListener();
if (useDefaultListener == null || useDefaultListener) {
readListenerList.add(new ModelBuildEventListener());
}
}
if (readBasicParameter.getCustomReadListenerList() != null
&& !readBasicParameter.getCustomReadListenerList().isEmpty()) {
this.readListenerList.addAll(readBasicParameter.getCustomReadListenerList());
}
if (parentAbstractReadHolder == null) {
setConverterMap(DefaultConverterLoader.loadDefaultReadConverter());
} else {
setConverterMap(new HashMap<String, Converter>(parentAbstractReadHolder.getConverterMap()));
}
if (readBasicParameter.getCustomConverterList() != null
&& !readBasicParameter.getCustomConverterList().isEmpty()) {
for (Converter converter : readBasicParameter.getCustomConverterList()) {
getConverterMap().put(
ConverterKeyBuild.buildKey(converter.supportJavaTypeKey(), converter.supportExcelTypeKey()),
converter);
}
}
}
在this.excelReadHeadProperty = new ExcelReadHeadProperty(this, getClazz(), getHead(), convertAllFiled);
时会初始化属性,调用ExcelHeadProperty
的构造函数,里面有个initColumnProperties(holder, convertAllFiled);
函数。
public ExcelHeadProperty(Holder holder, Class headClazz, List<List<String>> head, Boolean convertAllFiled) {
this.headClazz = headClazz;
headMap = new TreeMap<Integer, Head>();
contentPropertyMap = new TreeMap<Integer, ExcelContentProperty>();
fieldNameContentPropertyMap = new HashMap<String, ExcelContentProperty>();
ignoreMap = new HashMap<String, Field>(16);
headKind = HeadKindEnum.NONE;
headRowNumber = 0;
if (head != null && !head.isEmpty()) {
int headIndex = 0;
for (int i = 0; i < head.size(); i++) {
if (holder instanceof AbstractWriteHolder) {
if (((AbstractWriteHolder) holder).ignore(null, i)) {
continue;
}
}
headMap.put(headIndex, new Head(headIndex, null, head.get(i), Boolean.FALSE, Boolean.TRUE));
contentPropertyMap.put(headIndex, null);
headIndex++;
}
headKind = HeadKindEnum.STRING;
}
// convert headClazz to head
initColumnProperties(holder, convertAllFiled);
initHeadRowNumber();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("The initialization sheet/table 'ExcelHeadProperty' is complete , head kind is {}", headKind);
}
}
这里面的initOneColumnProperty(entry.getKey(), entry.getValue(), indexFiledMap.containsKey(entry.getKey()));
方法,这里就是读取excelProperty的地方:
private void initOneColumnProperty(int index, Field field, Boolean forceIndex) {
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
List<String> tmpHeadList = new ArrayList<String>();
boolean notForceName = excelProperty == null || excelProperty.value().length <= 0
|| (excelProperty.value().length == 1 && StringUtils.isEmpty((excelProperty.value())[0]));
if (headMap.containsKey(index)) {
tmpHeadList.addAll(headMap.get(index).getHeadNameList());
} else {
if (notForceName) {
tmpHeadList.add(field.getName());
} else {
Collections.addAll(tmpHeadList, excelProperty.value());
}
}
Head head = new Head(index, field.getName(), tmpHeadList, forceIndex, !notForceName);
ExcelContentProperty excelContentProperty = new ExcelContentProperty();
if (excelProperty != null) {
Class<? extends Converter> convertClazz = excelProperty.converter();
if (convertClazz != AutoConverter.class) {
try {
Converter converter = convertClazz.newInstance();
excelContentProperty.setConverter(converter);
} catch (Exception e) {
throw new ExcelCommonException("Can not instance custom converter:" + convertClazz.getName());
}
}
}
excelContentProperty.setHead(head);
excelContentProperty.setField(field);
excelContentProperty
.setDateTimeFormatProperty(DateTimeFormatProperty.build(field.getAnnotation(DateTimeFormat.class)));
excelContentProperty
.setNumberFormatProperty(NumberFormatProperty.build(field.getAnnotation(NumberFormat.class)));
headMap.put(index, head);
contentPropertyMap.put(index, excelContentProperty);
fieldNameContentPropertyMap.put(field.getName(), excelContentProperty);
}
写的时候注解加载也是同理,在ExcelWriteHeadProperty类的构造函数里,可以看出,写的时候对注解的获取就比读多了很多,因为写的时候格式啥的就会有很多特殊要求。
public ExcelWriteHeadProperty(Holder holder, Class headClazz, List<List<String>> head, Boolean convertAllFiled) {
super(holder, headClazz, head, convertAllFiled);
if (getHeadKind() != HeadKindEnum.CLASS) {
return;
}
this.headRowHeightProperty =
RowHeightProperty.build((HeadRowHeight) headClazz.getAnnotation(HeadRowHeight.class));
this.contentRowHeightProperty =
RowHeightProperty.build((ContentRowHeight) headClazz.getAnnotation(ContentRowHeight.class));
this.onceAbsoluteMergeProperty =
OnceAbsoluteMergeProperty.build((OnceAbsoluteMerge) headClazz.getAnnotation(OnceAbsoluteMerge.class));
ColumnWidth parentColumnWidth = (ColumnWidth) headClazz.getAnnotation(ColumnWidth.class);
HeadStyle parentHeadStyle = (HeadStyle) headClazz.getAnnotation(HeadStyle.class);
HeadFontStyle parentHeadFontStyle = (HeadFontStyle) headClazz.getAnnotation(HeadFontStyle.class);
ContentStyle parentContentStyle = (ContentStyle) headClazz.getAnnotation(ContentStyle.class);
ContentFontStyle parentContentFontStyle = (ContentFontStyle) headClazz.getAnnotation(ContentFontStyle.class);
for (Map.Entry<Integer, ExcelContentProperty> entry : getContentPropertyMap().entrySet()) {
Integer index = entry.getKey();
ExcelContentProperty excelContentPropertyData = entry.getValue();
if (excelContentPropertyData == null) {
throw new IllegalArgumentException(
"Passing in the class and list the head, the two must be the same size.");
}
Field field = excelContentPropertyData.getField();
Head headData = getHeadMap().get(index);
ColumnWidth columnWidth = field.getAnnotation(ColumnWidth.class);
if (columnWidth == null) {
columnWidth = parentColumnWidth;
}
headData.setColumnWidthProperty(ColumnWidthProperty.build(columnWidth));
HeadStyle headStyle = field.getAnnotation(HeadStyle.class);
if (headStyle == null) {
headStyle = parentHeadStyle;
}
headData.setHeadStyleProperty(StyleProperty.build(headStyle));
HeadFontStyle headFontStyle = field.getAnnotation(HeadFontStyle.class);
if (headFontStyle == null) {
headFontStyle = parentHeadFontStyle;
}
headData.setHeadFontProperty(FontProperty.build(headFontStyle));
ContentStyle contentStyle = field.getAnnotation(ContentStyle.class);
if (contentStyle == null) {
contentStyle = parentContentStyle;
}
headData.setContentStyleProperty(StyleProperty.build(contentStyle));
ContentFontStyle contentFontStyle = field.getAnnotation(ContentFontStyle.class);
if (contentFontStyle == null) {
contentFontStyle = parentContentFontStyle;
}
headData.setContentFontProperty(FontProperty.build(contentFontStyle));
headData.setLoopMergeProperty(LoopMergeProperty.build(field.getAnnotation(ContentLoopMerge.class)));
// If have @NumberFormat, 'NumberStringConverter' is specified by default
if (excelContentPropertyData.getConverter() == null) {
NumberFormat numberFormat = field.getAnnotation(NumberFormat.class);
if (numberFormat != null) {
excelContentPropertyData.setConverter(DefaultConverterLoader.loadAllConverter()
.get(ConverterKeyBuild.buildKey(field.getType(), CellDataTypeEnum.STRING)));
}
}
}
}
很多时候我们完成一个功能可能有现成的轮子使用,但是仅仅用可能还不够,最好研究下源码,这样一来有定制化需求不会手忙脚乱,二来可以提升自己的技术功底。任何一个使用比较广泛的框架都有其读到之处,多学学总没坏处。