java excel框架_使用POI封装一个轻量级Excel解析框架

前言

通过前面的三篇文章,我们已经对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();

}

整体设计

1038edebfe03

类图

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解析框架.在做的过程中,相信你一定获益匪浅

全量代码

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值