该文章为本系列的第四篇
第一篇为 : Java POI操作Excel(User Model)
第二篇为 : Java POI操作Excel(Event Model)
第三篇为 : Java POI操作Excel(Event User Model)
前言
通过前面的三篇文章,我们已经对POI解析Excel有了不错的理解.这篇文章,我们就来自己封装一个Excel解析框架.
那为什么要自己做一个解析框架?这个问题的本质,我觉得应该从个人的商业模式讲起.
我们每天去工作,赚取工资,本质上是在用我们只去不回的时间和注意力来换取金钱.那如果我们想提升我们获取的回报,显而易见的方式就是提升时薪.而除此之外,还有一个升级的办法,那就是把一份时间卖出很多份.比如畅销书的作家,写一本书.时间只用了一次,但是却可以在写完之后仍然在产生回报.
那作为程序员,我们能否也使用这种思路去解决工作中的问题呢,当然可以,比如说,我们今天要做的,封装一个Excel解析框架就是这样一种思路.在我们可预期的后续工作中,Excel导入数据这种功能肯定是还会再写的.但是如果这次写完,下次遇到我还是去查资料,重新写.那不仅仅是重复劳动.这次遇到的坑,下次可能会难免再踩一些.而如果我们在这一次封装了自己的库.下次再遇到,我们可以直接使用.不仅可以节约时间,也不会踩到同样的坑.所以,让我们开始行动吧~
分析需求
在我们的工作中,对于Excel上传,我们会遇到的场景一般是把上传来的Excel进行解析,组装成一个对象,然后校验数据,转成Po,导入数据库.而这个流程中,我们的Excel解析框架要做的事情,实际上就是解析Excel和组装对象.我们希望我们只用一点点的代码,就可以把Excel解析完,并且可以自由选择使用Dom方式解析还是Sax方式.甚至希望可以不知道上传的Excel的版本.
接口定义
提供解析功能的接口,可以理解为是一个门面(Facade).
public interface IExcelParser {
List parse(IParserParam parserParam);
}
关于解析方法的参数规范.
上传的过程中,我们需要Excel的流,要解析完成后组装的对象的类型,Excel中有多少列的数据.要解析的Sheet,以及表头数据.
由于Excel是外部通过上传,所以一般情况下,我们会对表头数据进行校验.来达到功能的收敛,防止误操作,对系统造成影响.当然如果不想校验,在我们的解析框架中,也应该是支持的.
public interface IParserParam {
Integer FIRST_SHEET = 0;
InputStream getExcelInputStream();
Class getTargetClass();
Integer getColumnSize();
Integer getSheetNum();
List getHeader();
}
整体设计
IExcelParseHandler接口提供具体的解析服务.对上层的Parser屏蔽解析细节.
客户端代码
我们从调用端的代码进行分析,来达到管中规豹的效果.
@Test
public void testDomXlsx() {
parser = new ExcelDomParser<>();
IParserParam parserParam = DefaultParserParam.builder()
.excelInputStream(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("test01.xlsx"))
.columnSize(4)
.sheetNum(IParserParam.FIRST_SHEET)
.targetClass(User.class)
.header(User.getHeader())
.build();
List user = parser.parse(parserParam);
System.out.println(user);
}
User类:
public class User {
@ExcelField(index = 0)
private String name;
@ExcelField(index = 1)
private String age;
@ExcelField(index = 2)
private String gender;
@ExcelField(index = 3, type = ExcelField.ExcelFieldType.Date)
private String dateStr;
客户端代码十分简单,我们只需要组装一个IParserParam的默认对象,DefaultParserParam.然后传入到Parser中即可解析完成.
再看看User类.User类的字段上出现了ExcelField注解.我们都知道要想把一行数据转成对象,使用反射是最简单的方式,所以ExcelField就是对应字段和在Excel中的列数使用.
至于为什么字段都定义为String,因为后续还要转对象为Po.在Excel上传解析这个地方使用String类型最为方便.
线程安全问题
在Web项目中使用我们的框架,必然是要与Spring进行整合.在整合的时候Spring会默认给我们创建单例的解析类.而我们要做的就是保证这个单例的解析类不会存在线程安全问题.那这是怎么实现的呢.
我们先来看下dom解析的方式
public class ExcelDomParser extends AbstractExcelParser {
private IExcelParseHandler excelParseHandler;
public ExcelDomParser() {
this.excelParseHandler = new ExcelDomParseHandler<>();
}
@Override
protected IExcelParseHandler createHandler(InputStream excelInputStream) {
return this.excelParseHandler;
}
}
上面是上层DomParser的代码,根据代码我们可以发现,excelParseHandler是成员变量.一直都是使用的一个.那接下来我们再看一下DomparseHandler的实现.
public class ExcelDomParseHandler extends BaseExcelParseHandler {
@Override
public List process(IParserParam parserParam) throws Exception {
Workbook workbook = generateWorkBook(parserParam);
Sheet sheet = workbook.getSheetAt(parserParam.getSheetNum());
Iterator rowIterator = sheet.rowIterator();
if (parserParam.getHeader() != null && parserParam.getHeader().size() != 0) {
checkHeader(rowIterator, parserParam);
}
return parseRowToTargetList(rowIterator, parserParam);
}
private void checkHeader(Iterator rowIterator, IParserParam parserParam) {
while (true) {
Row row = rowIterator.next();
List rowData = parseRowToList(row, parserParam.getColumnSize());
boolean empty = isRowDataEmpty(rowData);
if (!empty) {
validHeader(parserParam, rowData);
break;
}
}
}
private Workbook generateWorkBook(IParserParam parserParam) throws IOException, InvalidFormatException {
return WorkbookFactory.create(parserParam.getExcelInputStream());
}
private List parseRowToTargetList(Iterator rowIterator, IParserParam parserParam) throws InstantiationException, IllegalAccessException {
List result = new ArrayList<>();
for (; rowIterator.hasNext(); ) {
Row row = rowIterator.next();
List rowData = parseRowToList(row, parserParam.getColumnSize());
Optional d = parseRowToTarget(parserParam, rowData);
d.ifPresent(result::add);
}
return result;
}
private List parseRowToList(Row row, int size) {
List dataRow = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
if (row.getCell(i) != null) {
DataFormatter formatter = new DataFormatter();
String formattedCellValue = formatter.formatCellValue(row.getCell(i));
dataRow.add(formattedCellValue.trim());
} else {
dataRow.add("");
}
}
return dataRow;
}
}
我们通过代码看到DomParseHandler本身没有使用任何的成员变量,而父类BaseExcelParseHandler中存在的一个成员变量head,也没有在这个类中使用.所以这个类在多线程环境下是安全的.不会存在问题.
接下来我们看一下Sax解析的Parser
public class ExcelSaxParser extends AbstractExcelParser {
public IExcelParseHandler createHandler(InputStream excelInputStream) {
try {
byte[] header8 = IOUtils.peekFirst8Bytes(excelInputStream);
if (NPOIFSFileSystem.hasPOIFSHeader(header8)) {
return new Excel2003ParseHandler<>();
} else if (DocumentFactoryHelper.hasOOXMLHeader(excelInputStream)) {
return new Excel2007ParseHandler<>();
} else {
throw new IllegalArgumentException("Your InputStream was neither an OLE2 stream, nor an OOXML stream");
}
} catch (Exception e) {
logger.error("getParserInstance Error!", e);
throw new RuntimeException(e);
}
}
}
通过代码,我们发现,每次都会创建一个新的Handler,并且根据不同判断使用不同的Handler.这种方式在多线程环境下也不会存在问题.可以使用Spring的单例进行管理
与Spring整合
使用Dom方式
@Autowire
private IExcelParser excelParser;
使用Sax方式
@Autowire
private IExcelParser excelParser;
总结
由于代码比较多,所以不能面面俱到的讲解所有的细节,但是看完整篇文章,相信你对如何封装也有了一定的想法,可以去尝试着实现属于你自己的Excel解析框架.在做的过程中,相信你一定获益匪浅
全量代码
https://github.com/amlongjie/ExcelParser