使用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<T> {
    List<T> parse(IParserParam parserParam);
}

关于解析方法的参数规范.
上传的过程中,我们需要Excel的流,要解析完成后组装的对象的类型,Excel中有多少列的数据.要解析的Sheet,以及表头数据.

由于Excel是外部通过上传,所以一般情况下,我们会对表头数据进行校验.来达到功能的收敛,防止误操作,对系统造成影响.当然如果不想校验,在我们的解析框架中,也应该是支持的.

public interface IParserParam {

    Integer FIRST_SHEET = 0;

    InputStream getExcelInputStream();

    Class getTargetClass();

    Integer getColumnSize();

    Integer getSheetNum();

    List<String> 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> 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<T> extends AbstractExcelParser<T> {

    private IExcelParseHandler<T> excelParseHandler;

    public ExcelDomParser() {
        this.excelParseHandler = new ExcelDomParseHandler<>();
    }

    @Override
    protected IExcelParseHandler<T> createHandler(InputStream excelInputStream) {
        return this.excelParseHandler;
    }
}

上面是上层DomParser的代码,根据代码我们可以发现,excelParseHandler是成员变量.一直都是使用的一个.那接下来我们再看一下DomparseHandler的实现.

public class ExcelDomParseHandler<T> extends BaseExcelParseHandler<T> {

    @Override
    public List<T> process(IParserParam parserParam) throws Exception {
        Workbook workbook = generateWorkBook(parserParam);
        Sheet sheet = workbook.getSheetAt(parserParam.getSheetNum());
        Iterator<Row> rowIterator = sheet.rowIterator();
        if (parserParam.getHeader() != null && parserParam.getHeader().size() != 0) {
            checkHeader(rowIterator, parserParam);
        }
        return parseRowToTargetList(rowIterator, parserParam);
    }

    private void checkHeader(Iterator<Row> rowIterator, IParserParam parserParam) {
        while (true) {
            Row row = rowIterator.next();
            List<String> 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<T> parseRowToTargetList(Iterator<Row> rowIterator, IParserParam parserParam) throws InstantiationException, IllegalAccessException {
        List<T> result = new ArrayList<>();
        for (; rowIterator.hasNext(); ) {
            Row row = rowIterator.next();
            List<String> rowData = parseRowToList(row, parserParam.getColumnSize());
            Optional<T> d = parseRowToTarget(parserParam, rowData);
            d.ifPresent(result::add);
        }
        return result;
    }

    private List<String> parseRowToList(Row row, int size) {
        List<String> 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<T> extends AbstractExcelParser<T> {

    public IExcelParseHandler<T> 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方式

<bean id = "excelParser" class="com.snakotech.excelhelper.ExcelDomparser">

@Autowire
private IExcelParser excelParser;

使用Sax方式

<bean id = "excelParser" class="com.snakotech.excelhelper.ExcelSaxparser">

@Autowire
private IExcelParser excelParser;

总结

由于代码比较多,所以不能面面俱到的讲解所有的细节,但是看完整篇文章,相信你对如何封装也有了一定的想法,可以去尝试着实现属于你自己的Excel解析框架.在做的过程中,相信你一定获益匪浅

全量代码

https://github.com/amlongjie/ExcelParser

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,800评论 6 342
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,009评论 25 707
  • 该文章为本系列的第二篇第一篇为 : Java POI操作Excel(User Model)第三篇为 : Java ...
    mmlmml阅读 9,415评论 0 5
  • 该文章为本系列的第一篇第二篇为 : Java POI操作Excel(Event Model)第三篇为 : Java...
    mmlmml阅读 13,289评论 6 21
  • 在墙的一角 阳光永远照不到的地方 有一抹绿 渺小而可悲 有气无力地把手伸向天空 渴望着墙的尽头 那个与天连在一起的...
    无尽De华尔兹阅读 302评论 0 0