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形成映射。所以我希望函數(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)折騰源碼了涩堤。: )