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

该文章为本系列的第四篇
第一篇为 : 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();
}

整体设计

使用POI封装一个轻量级Excel解析框架_第1张图片
类图

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

你可能感兴趣的:(使用POI封装一个轻量级Excel解析框架)