使用POI封裝一個(gè)輕量級Excel解析框架

該文章為本系列的第四篇
第一篇為 : Java POI操作Excel(User Model)
第二篇為 : Java POI操作Excel(Event Model)
第三篇為 : Java POI操作Excel(Event User Model)

前言

通過前面的三篇文章,我們已經(jīng)對POI解析Excel有了不錯(cuò)的理解.這篇文章,我們就來自己封裝一個(gè)Excel解析框架.

那為什么要自己做一個(gè)解析框架?這個(gè)問題的本質(zhì),我覺得應(yīng)該從個(gè)人的商業(yè)模式講起.

我們每天去工作,賺取工資,本質(zhì)上是在用我們只去不回的時(shí)間和注意力來換取金錢.那如果我們想提升我們獲取的回報(bào),顯而易見的方式就是提升時(shí)薪.而除此之外,還有一個(gè)升級的辦法,那就是把一份時(shí)間賣出很多份.比如暢銷書的作家,寫一本書.時(shí)間只用了一次,但是卻可以在寫完之后仍然在產(chǎn)生回報(bào).

那作為程序員,我們能否也使用這種思路去解決工作中的問題呢,當(dāng)然可以,比如說,我們今天要做的,封裝一個(gè)Excel解析框架就是這樣一種思路.在我們可預(yù)期的后續(xù)工作中,Excel導(dǎo)入數(shù)據(jù)這種功能肯定是還會(huì)再寫的.但是如果這次寫完,下次遇到我還是去查資料,重新寫.那不僅僅是重復(fù)勞動(dòng).這次遇到的坑,下次可能會(huì)難免再踩一些.而如果我們在這一次封裝了自己的庫.下次再遇到,我們可以直接使用.不僅可以節(jié)約時(shí)間,也不會(huì)踩到同樣的坑.所以,讓我們開始行動(dòng)吧~

分析需求

在我們的工作中,對于Excel上傳,我們會(huì)遇到的場景一般是把上傳來的Excel進(jìn)行解析,組裝成一個(gè)對象,然后校驗(yàn)數(shù)據(jù),轉(zhuǎn)成Po,導(dǎo)入數(shù)據(jù)庫.而這個(gè)流程中,我們的Excel解析框架要做的事情,實(shí)際上就是解析Excel和組裝對象.我們希望我們只用一點(diǎn)點(diǎn)的代碼,就可以把Excel解析完,并且可以自由選擇使用Dom方式解析還是Sax方式.甚至希望可以不知道上傳的Excel的版本.

接口定義

提供解析功能的接口,可以理解為是一個(gè)門面(Facade).

public interface IExcelParser<T> {
    List<T> parse(IParserParam parserParam);
}

關(guān)于解析方法的參數(shù)規(guī)范.
上傳的過程中,我們需要Excel的流,要解析完成后組裝的對象的類型,Excel中有多少列的數(shù)據(jù).要解析的Sheet,以及表頭數(shù)據(jù).

由于Excel是外部通過上傳,所以一般情況下,我們會(huì)對表頭數(shù)據(jù)進(jìn)行校驗(yàn).來達(dá)到功能的收斂,防止誤操作,對系統(tǒng)造成影響.當(dāng)然如果不想校驗(yàn),在我們的解析框架中,也應(yīng)該是支持的.

public interface IParserParam {

    Integer FIRST_SHEET = 0;

    InputStream getExcelInputStream();

    Class getTargetClass();

    Integer getColumnSize();

    Integer getSheetNum();

    List<String> getHeader();
}

整體設(shè)計(jì)

類圖

IExcelParseHandler接口提供具體的解析服務(wù).對上層的Parser屏蔽解析細(xì)節(jié).

客戶端代碼

我們從調(diào)用端的代碼進(jìn)行分析,來達(dá)到管中規(guī)豹的效果.

     @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;

客戶端代碼十分簡單,我們只需要組裝一個(gè)IParserParam的默認(rèn)對象,DefaultParserParam.然后傳入到Parser中即可解析完成.

再看看User類.User類的字段上出現(xiàn)了ExcelField注解.我們都知道要想把一行數(shù)據(jù)轉(zhuǎn)成對象,使用反射是最簡單的方式,所以ExcelField就是對應(yīng)字段和在Excel中的列數(shù)使用.

至于為什么字段都定義為String,因?yàn)楹罄m(xù)還要轉(zhuǎn)對象為Po.在Excel上傳解析這個(gè)地方使用String類型最為方便.

線程安全問題

在Web項(xiàng)目中使用我們的框架,必然是要與Spring進(jìn)行整合.在整合的時(shí)候Spring會(huì)默認(rèn)給我們創(chuàng)建單例的解析類.而我們要做的就是保證這個(gè)單例的解析類不會(huì)存在線程安全問題.那這是怎么實(shí)現(xiàn)的呢.

我們先來看下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的代碼,根據(jù)代碼我們可以發(fā)現(xiàn),excelParseHandler是成員變量.一直都是使用的一個(gè).那接下來我們再看一下DomparseHandler的實(shí)現(xiàn).

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中存在的一個(gè)成員變量head,也沒有在這個(gè)類中使用.所以這個(gè)類在多線程環(huán)境下是安全的.不會(huì)存在問題.

接下來我們看一下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);
        }
    }
    
}

通過代碼,我們發(fā)現(xiàn),每次都會(huì)創(chuàng)建一個(gè)新的Handler,并且根據(jù)不同判斷使用不同的Handler.這種方式在多線程環(huán)境下也不會(huì)存在問題.可以使用Spring的單例進(jìn)行管理

與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;

總結(jié)

由于代碼比較多,所以不能面面俱到的講解所有的細(xì)節(jié),但是看完整篇文章,相信你對如何封裝也有了一定的想法,可以去嘗試著實(shí)現(xiàn)屬于你自己的Excel解析框架.在做的過程中,相信你一定獲益匪淺

全量代碼

https://github.com/amlongjie/ExcelParser

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末村怪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子浮庐,更是在濱河造成了極大的恐慌甚负,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件兔辅,死亡現(xiàn)場離奇詭異腊敲,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)维苔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門碰辅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人介时,你說我怎么就攤上這事没宾×璞颍” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵循衰,是天一觀的道長铲敛。 經(jīng)常有香客問我,道長会钝,這世上最難降的妖魔是什么伐蒋? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮迁酸,結(jié)果婚禮上先鱼,老公的妹妹穿的比我還像新娘。我一直安慰自己奸鬓,他們只是感情好焙畔,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著串远,像睡著了一般宏多。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上澡罚,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天伸但,我揣著相機(jī)與錄音,去河邊找鬼始苇。 笑死砌烁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的催式。 我是一名探鬼主播函喉,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼荣月!你這毒婦竟也來了管呵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤哺窄,失蹤者是張志新(化名)和其女友劉穎捐下,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萌业,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡坷襟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了生年。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片婴程。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抱婉,靈堂內(nèi)的尸體忽然破棺而出档叔,到底是詐尸還是另有隱情桌粉,我是刑警寧澤,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布衙四,位于F島的核電站铃肯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏传蹈。R本人自食惡果不足惜押逼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卡睦。 院中可真熱鬧宴胧,春花似錦、人聲如沸表锻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瞬逊。三九已至,卻和暖如春仪或,著一層夾襖步出監(jiān)牢的瞬間确镊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工范删, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蕾域,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓到旦,卻偏偏與公主長得像旨巷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子添忘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容

  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,846評論 6 342
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,280評論 25 707
  • 該文章為本系列的第二篇第一篇為 : Java POI操作Excel(User Model)第三篇為 : Java ...
    mmlmml閱讀 9,438評論 0 5
  • 該文章為本系列的第一篇第二篇為 : Java POI操作Excel(Event Model)第三篇為 : Java...
    mmlmml閱讀 13,433評論 6 21
  • 在墻的一角 陽光永遠(yuǎn)照不到的地方 有一抹綠 渺小而可悲 有氣無力地把手伸向天空 渴望著墻的盡頭 那個(gè)與天連在一起的...
    無盡De華爾茲閱讀 308評論 0 0