從Excel的讀寫(xiě)來(lái)看Kotlin的擴(kuò)展方法

Apache POI框架是非常常用的操作Office文檔庫(kù)爹土,最近用Kotlin重構(gòu)了一段使用POI操作Excel的方法辕棚,讓代碼的構(gòu)建上簡(jiǎn)化了很多焦人。

先來(lái)觀察一下之前使用Java中POI操作Excel要經(jīng)歷什么:

public static void writeToOutputStream(ExcelData data, OutputStream outputStream) throws IOException {
         // 創(chuàng)建一個(gè)工作簿
        XSSFWorkbook xs = new XSSFWorkbook();

        // 設(shè)置一個(gè)樣式
        DataFormat fmt = xs.createDataFormat();
        CellStyle textStyle = xs.createCellStyle();
        textStyle.setDataFormat(fmt.getFormat("@"));
        
        // 操作Excel
        for (ExcelSheet sheet : data.sheets) {
             Sheet xsheet = xs.createSheet(sheet.sheetName);
            int rowNum = 0;
            // 填寫(xiě)每行的內(nèi)容
            for (ExcelRow row : sheet.rows) {
                 Row xrow = xsheet.createRow(rowNum);
                int cellNum = 0;
                xsheet.setDefaultColumnStyle(cellNum, textStyle);
                for (ExcelColumn column : row.columns) {
                     Cell cell = xrow.createCell(cellNum);
                    if (column != null && column.value instanceof Number) {
                        cell.setCellValue(((Number) column.value).doubleValue());
                    } else if (column != null) {
                        cell.setCellValue(column.value.toString());
                    } else {
                        cell.setCellValue("null");
                    }
                    cellNum += 1;
                }
                rowNum += 1;
            }
        }
        xs.write(outputStream);
}

代碼看起來(lái)就不是特別直觀竖独,以致于我需要額外抽象出一系列ExcelData這種Value Object來(lái)存放數(shù)據(jù)傳入這個(gè)方法(writeToOutputStream())以降低調(diào)用方的使用困惑赛蔫。

而在使用Kotlin來(lái)重構(gòu)這部分代碼的時(shí)候砂客,可以充分地利用Kotlin擴(kuò)展方法的特性擴(kuò)展POI這個(gè)庫(kù),那么可以先來(lái)了解下Kotlin的擴(kuò)展方法呵恢。在業(yè)務(wù)中我們經(jīng)常要遇到各種Utils類鞠值,這些Utils的本質(zhì)是對(duì)原有類的擴(kuò)展,比如下面這個(gè)StringUtils:

String str = "hello";
boolean flag = StringUtils.isNotEmpty(str);

光是在我們項(xiàng)目中就找到了三種StringUtils實(shí)現(xiàn)渗钉,如果我們可以擴(kuò)展原有的類庫(kù)彤恶,而不是寫(xiě)Utils,語(yǔ)言上表現(xiàn)力會(huì)好很多鳄橘。在Kotlin中允許定義這種擴(kuò)展函數(shù)來(lái)擴(kuò)充原來(lái)的已經(jīng)存在類庫(kù)声离,而不是通過(guò)Utils的方式來(lái)補(bǔ)充:

fun String.isNotEmpty() = this != null && this.length > 0
val str = "hello"
var flag = str.isNotEmpty()

這樣的寫(xiě)法更加符合面向?qū)ο?不過(guò)由于Jvm本身的限制,kotlin的這種寫(xiě)法挥唠,最終會(huì)被JVM編譯成Utils的的類抵恋,在Java中還是會(huì)變成Utils被使用。)宝磨。

再回到POI的例子弧关,對(duì)于調(diào)用方來(lái)說(shuō),什么樣的方式創(chuàng)建Excel才是直觀的呢唤锉?

Excel

既然Excel是一張表格世囊,那么我們的代碼是不是也可以弄得跟表格一樣和Excel形成映射。所以我希望函數(shù)的最終使用效果可以是這樣:

Excel.create {
    val sheet = it.createSheet()
    
    // sheet[row,column] = value
    sheet[0, 0] = "標(biāo)題1"
    sheet[0, 1] = "標(biāo)題2"
    sheet[0, 2] = "標(biāo)題3"
    sheet[0, 3] = "標(biāo)題4"
}

在Kotlin中用擴(kuò)展方法非常少的代碼就足夠完成這套擴(kuò)展了:

// operator關(guān)鍵字允許我們對(duì)操作符號(hào)(類似 + , - 等等)進(jìn)行重載
// a.get(i) 在對(duì)用的操作符號(hào)是 a[i] 
// 操作符號(hào)重載的方法列表具體參見(jiàn):https://kotlinlang.org/docs/reference/operator-overloading.html
operator fun Sheet.get(n: Int): Row = this.getRow(n) ?: this.createRow(n)

// 就像前面String一樣窿祥,在**方法名稱**前面加上一個(gè)類名株憾,將這個(gè)方法添加到Row這個(gè)類中。
// 在POI中晒衩,Sheet嗤瞎,Row,Workbook等等原本都是沒(méi)有set和get方法的
// 這幾個(gè)方法都是試圖去獲取Excel中某一行或者某一個(gè)工作簿听系,如果沒(méi)有不存在就創(chuàng)建它們
operator fun Row.get(n: Int): Cell =
        this.getCell(n) ?: this.createCell(n, Cell.CELL_TYPE_BLANK)

operator fun Sheet.get(x: Int, y: Int): Cell = this[x][y]

operator fun Workbook.get(n: Int): Sheet =
        try {
            this.getSheetAt(n)
        } catch (e: Exception) {
            this.createSheet()
        }

// 在Kotlin中`?.method()`這種調(diào)用方式可以避免空指針異常贝奇,如果對(duì)象是null,方法不會(huì)被調(diào)用靠胜。
operator fun Sheet.set(x: Int, y: Int, value: Any?) {
    value?.let {
        // 所有的字段都設(shè)置成文本字段
        val textStyle = this.workbook.createCellStyle()
        textStyle.dataFormat = this.workbook.createDataFormat().getFormat("@")
        this.setDefaultColumnStyle(x, textStyle)
        val cell = this[x, y]
        when (value) {
            // 判斷value的數(shù)據(jù)類型掉瞳,然后用轉(zhuǎn)換之后填入
            is String -> cell.setCellValue(value)
            is Int -> cell.setCellValue(value.toString())
            is Double -> cell.setCellValue(value.toString())
            else -> throw IllegalArgumentException("數(shù)據(jù)類型不支持")
        }
    }
}

// object 關(guān)鍵字可以將Excel這個(gè)類定義成一個(gè)單例對(duì)象毕源,和Scala一樣。
object Excel {
    // 這個(gè)方法接收一個(gè) 入?yún)⑹莁Workbook`類陕习,返回值是空的**函數(shù)對(duì)象** 
    fun create(opFun: (Workbook) -> Unit): ByteArray =
            XSSFWorkbook().use {
                it
                        .apply(opFun) // 利用apply方法把自己傳入`opFun()`方法里
                        .let { workbook ->
                            ByteArrayOutputStream()
                                    .use {
                                        workbook.write(it)
                                        // 返回一個(gè)byte[]對(duì)象霎褐,即Excel文件
                                        return it.toByteArray()
                                    }
                        }
            }
}

所以再看調(diào)用方的那一段代碼, 他就能直接獲取到一個(gè)可以寫(xiě)到本地的Excel文件流:

val excelFileData = Excel
                .create {
                    val sheet = it.createSheet()
                    // sheet[row,column] = value
                    sheet[0, 0] = "標(biāo)題1"
                    sheet[0, 1] = "標(biāo)題2"
                    sheet[0, 2] = "標(biāo)題3"
                    sheet[0, 3] = "標(biāo)題4"
}

Files.write(Paths.get("/path/to/demo.xlsx"), excelFileData)

和之前Java的版本比起來(lái),這樣寫(xiě)法顯得清楚很多了该镣,而且一般項(xiàng)目中需要制作Excel的業(yè)務(wù)冻璃,數(shù)據(jù)勢(shì)必是以List或者Map為主, 完全可以通過(guò)遍歷對(duì)象的方式來(lái)簡(jiǎn)化這種Excel操作,看看Java中使用這段效果的代碼:

Excel.INSTANCE.create(workbook -> {
            final Sheet sheet = workbook.createSheet();
            // ExcelIntegrationKt.set(sheet,row,column,value);
            ExcelIntegrationKt.set(sheet, 0, 0, "標(biāo)題1");
            ExcelIntegrationKt.set(sheet, 0, 1, "標(biāo)題2");
            ExcelIntegrationKt.set(sheet, 0, 2, "標(biāo)題3");
            ExcelIntegrationKt.set(sheet, 0, 3, "標(biāo)題4");
            return Unit.INSTANCE;
});

好像看起來(lái)也不是很差嘛损合,哈哈哈俱饿。通過(guò)這個(gè)例子,我不是為了說(shuō)這個(gè)Excel的操作有多好用塌忽,而是希望寫(xiě)寫(xiě)這種擴(kuò)展方法的好處。當(dāng)然擴(kuò)展方法也不是Kotlin獨(dú)創(chuàng)的失驶,很多語(yǔ)言都支持這種方式土居,在Ruby中這種寫(xiě)法被稱為Monkey Patch(猴子打補(bǔ)丁),但在Jvm上的語(yǔ)言出現(xiàn)這種特性還是能很直接地給我們帶來(lái)福利的嬉探,這種擴(kuò)展方法擦耀,可以讓我們對(duì)一些第三方庫(kù)進(jìn)行簡(jiǎn)單擴(kuò)展的時(shí)候,不用像之前在Java中大費(fèi)周章來(lái)折騰源碼了涩堤。: )

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末眷蜓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子胎围,更是在濱河造成了極大的恐慌吁系,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件白魂,死亡現(xiàn)場(chǎng)離奇詭異汽纤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)福荸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門蕴坪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人敬锐,你說(shuō)我怎么就攤上這事背传。” “怎么了台夺?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵径玖,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我谒养,道長(zhǎng)挺狰,這世上最難降的妖魔是什么明郭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮丰泊,結(jié)果婚禮上薯定,老公的妹妹穿的比我還像新娘。我一直安慰自己瞳购,他們只是感情好话侄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著学赛,像睡著了一般年堆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盏浇,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天变丧,我揣著相機(jī)與錄音,去河邊找鬼绢掰。 笑死痒蓬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的滴劲。 我是一名探鬼主播攻晒,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼班挖!你這毒婦竟也來(lái)了鲁捏?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤萧芙,失蹤者是張志新(化名)和其女友劉穎给梅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體末购,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡破喻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盟榴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片曹质。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖擎场,靈堂內(nèi)的尸體忽然破棺而出羽德,到底是詐尸還是另有隱情,我是刑警寧澤迅办,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布宅静,位于F島的核電站,受9級(jí)特大地震影響站欺,放射性物質(zhì)發(fā)生泄漏姨夹。R本人自食惡果不足惜纤垂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望磷账。 院中可真熱鬧峭沦,春花似錦、人聲如沸逃糟。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绰咽。三九已至菇肃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間取募,已是汗流浹背琐谤。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留玩敏,地道東北人笑跛。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像聊品,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子几苍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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