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

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

前言

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

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

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

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

分析需求

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

接口定義

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

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

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

由于Excel是外部通過(guò)上傳,所以一般情況下,我們會(huì)對(duì)表頭數(shù)據(jù)進(jìn)行校驗(yàn).來(lái)達(dá)到功能的收斂,防止誤操作,對(duì)系統(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ù).對(duì)上層的Parser屏蔽解析細(xì)節(jié).

客戶端代碼

我們從調(diào)用端的代碼進(jìn)行分析,來(lái)達(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;

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

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

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

線程安全問(wèn)題

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

我們先來(lái)看下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è).那接下來(lái)我們?cè)倏匆幌翫omparseHandler的實(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;
    }
}

我們通過(guò)代碼看到DomParseHandler本身沒(méi)有使用任何的成員變量,而父類BaseExcelParseHandler中存在的一個(gè)成員變量head,也沒(méi)有在這個(gè)類中使用.所以這個(gè)類在多線程環(huán)境下是安全的.不會(huì)存在問(wèn)題.

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

通過(guò)代碼,我們發(fā)現(xiàn),每次都會(huì)創(chuàng)建一個(gè)新的Handler,并且根據(jù)不同判斷使用不同的Handler.這種方式在多線程環(huán)境下也不會(huì)存在問(wèn)題.可以使用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é),但是看完整篇文章,相信你對(duì)如何封裝也有了一定的想法,可以去嘗試著實(shí)現(xiàn)屬于你自己的Excel解析框架.在做的過(guò)程中,相信你一定獲益匪淺

全量代碼

https://github.com/amlongjie/ExcelParser

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容