如何優雅的導出Excel

前言

公司項目最近有一個需要:報表導出。整個系統下來,起碼超過一百張報表需要導出。這個時候如何優雅的實現報表導出,釋放生產力就顯得很重要了。下面主要給大家分享一下該工具類的使用方法與實現思路。

實現的功能點

對於每個報表都相同的操作,我們很自然的會抽離出來,這個很簡單。而最重要的是:如何把那些每個報表不相同的操作進行良好的封裝,儘可能的提高複用性;針對以上的原則,主要實現了一下關鍵功能點:

導出任意類型的數據
自由設置表頭
自由設置字段的導出格式
使用實例

上面說到了本工具類實現了三個功能點,自然在使用的時候設置好這三個要點即可:

設置數據列表
設置表頭
設置字段格式
下面的export函數可以直接向客戶端返回一個excel數據,其中productInfoPos爲待導出的數據列表,ExcelHeaderInfo用來保存表頭信息,包括表頭名稱,表頭的首列,尾列,首行,尾行。

因爲默認導出的數據格式都是字符串型,所以還需要一個Map參數用來指定某個字段的格式化類型(例如數字類型,小數類型、日期類型)。這裏大家知道個大概怎麼使用就好了,下面會對這些參數進行詳細解釋。

@Override
public void export(HttpServletResponse response, String fileName) {
// 待導出數據
List productInfoPos = this.multiThreadListProduct();
ExcelUtils excelUtils = new ExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo());
excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook());
}
// 獲取表頭信息
private List getHeaderInfo() {
return Arrays.asList(
new ExcelHeaderInfo(1, 1, 0, 0, “id”),
new ExcelHeaderInfo(1, 1, 1, 1, “商品名稱”),
new ExcelHeaderInfo(0, 0, 2, 3, “分類”),
new ExcelHeaderInfo(1, 1, 2, 2, “類型ID”),
new ExcelHeaderInfo(1, 1, 3, 3, “分類名稱”),
new ExcelHeaderInfo(0, 0, 4, 5, “品牌”),
new ExcelHeaderInfo(1, 1, 4, 4, “品牌ID”),
new ExcelHeaderInfo(1, 1, 5, 5, “品牌名稱”),
new ExcelHeaderInfo(0, 0, 6, 7, “商店”),
new ExcelHeaderInfo(1, 1, 6, 6, “商店ID”),
new ExcelHeaderInfo(1, 1, 7, 7, “商店名稱”),
new ExcelHeaderInfo(1, 1, 8, 8, “價格”),
new ExcelHeaderInfo(1, 1, 9, 9, “庫存”),
new ExcelHeaderInfo(1, 1, 10, 10, “銷量”),
new ExcelHeaderInfo(1, 1, 11, 11, “插入時間”),
new ExcelHeaderInfo(1, 1, 12, 12, “更新時間”),
new ExcelHeaderInfo(1, 1, 13, 13, “記錄是否已經刪除”)
);
}
// 獲取格式化信息
private Map<String, ExcelFormat> getFormatInfo() {
Map<String, ExcelFormat> format = new HashMap<>();
format.put(“id”, ExcelFormat.FORMAT_INTEGER);
format.put(“categoryId”, ExcelFormat.FORMAT_INTEGER);
format.put(“branchId”, ExcelFormat.FORMAT_INTEGER);
format.put(“shopId”, ExcelFormat.FORMAT_INTEGER);
format.put(“price”, ExcelFormat.FORMAT_DOUBLE);
format.put(“stock”, ExcelFormat.FORMAT_INTEGER);
format.put(“salesNum”, ExcelFormat.FORMAT_INTEGER);
format.put(“isDel”, ExcelFormat.FORMAT_INTEGER);
return format;
}
實現效果

如何優雅的導出Excel

源碼分析

哈哈,自己分析自己的代碼,有點意思。由於不方便貼出太多的代碼,大家可以先到github上clone源碼,再回來閱讀文章。

✨源碼地址✨

https://github.com/dearKundy/excel-utils

LZ使用的poi 4.0.1版本的這個工具,想要實用海量數據的導出自然得使用SXSSFWorkbook這個組件。關於poi的具體用法在這裏我就不多說了,這裏主要是給大家講解如何對poi進行封裝使用。

成員變量

我們重點看ExcelUtils這個類,這個類是實現導出的核心,先來看一下三個成員變量。

private List list;
private List excelHeaderInfos;
private Map<String, ExcelFormat> formatInfo;
list

該成員變量用來保存待導出的數據。

ExcelHeaderInfo

該成員變量主要用來保存表頭信息,因爲我們需要定義多個表頭信息,所以需要使用一個列表來保存,ExcelHeaderInfo構造函數如下

ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)

firstRow:該表頭所佔位置的首行
lastRow:該表頭所佔位置的尾行
firstCol:該表頭所佔位置的首列
lastCol:該表頭所佔位置的尾行
title:該表頭的名稱
ExcelFormat

該參數主要用來格式化字段,我們需要預先約定好轉換成那種格式,不能隨用戶自己定。所以我們定義了一個枚舉類型的變量,該枚舉類只有一個字符串類型成員變量,用來保存想要轉換的格式,例如FORMAT_INTEGER就是轉換成整型。因爲我們需要接受多個字段的轉換格式,所以定義了一個Map類型來接收,該參數可以省略(默認格式爲字符串)。

public enum ExcelFormat {
FORMAT_INTEGER(“INTEGER”),
FORMAT_DOUBLE(“DOUBLE”),
FORMAT_PERCENT(“PERCENT”),
FORMAT_DATE(“DATE”);
private String value;
ExcelFormat(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
核心方法

  1. 創建表頭

該方法用來初始化表頭,而創建表頭最關鍵的就是poi中Sheet類的addMergedRegion(CellRangeAddress var1)方法,該方法用於單元格融合。

我們會遍歷ExcelHeaderInfo列表,按照每個ExcelHeaderInfo的座標信息進行單元格融合,然後在融合之後的每個單元首行和首列的位置創建單元格,然後爲單元格賦值即可,通過上面的步驟就完成了任意類型的表頭設置。

// 創建表頭
private void createHeader(Sheet sheet, CellStyle style) {
for (ExcelHeaderInfo excelHeaderInfo : excelHeaderInfos) {
Integer lastRow = excelHeaderInfo.getLastRow();
Integer firstRow = excelHeaderInfo.getFirstRow();
Integer lastCol = excelHeaderInfo.getLastCol();
Integer firstCol = excelHeaderInfo.getFirstCol();
// 行距或者列距大於0才進行單元格融合
if ((lastRow - firstRow) != 0 || (lastCol - firstCol) != 0) {
sheet.addMergedRegion(new CellRangeAddress(firstRow, lastRow, firstCol, lastCol));
}
// 獲取當前表頭的首行位置
Row row = sheet.getRow(firstRow);
// 在表頭的首行與首列位置創建一個新的單元格
Cell cell = row.createCell(firstCol);
// 賦值單元格
cell.setCellValue(excelHeaderInfo.getTitle());
cell.setCellStyle(style);
sheet.setColumnWidth(firstCol, sheet.getColumnWidth(firstCol) * 17 / 12);
}
}
2. 轉換數據

在進行正文賦值之前,我們先要對原始數據列表轉換成字符串的二維數組,之所以轉成字符串格式是因爲可以統一的處理各種類型,之後有需要我們再轉換回來即可。

// 將原始數據轉成二維數組
private String[][] transformData() {
int dataSize = this.list.size();
String[][] datas = new String[dataSize][];
// 獲取報表的列數
Field[] fields = list.get(0).getClass().getDeclaredFields();
// 獲取實體類的字段名稱數組
List columnNames = this.getBeanProperty(fields);
for (int i = 0; i < dataSize; i++) {
datas[i] = new String[fields.length];
for (int j = 0; j < fields.length; j++) {
try {
// 賦值
datas[i][j] = BeanUtils.getProperty(list.get(i), columnNames.get(j));
} catch (Exception e) {
LOGGER.error(“獲取對象屬性值失敗”);
e.printStackTrace();
}
}
}
return datas;
}
這個方法中我們通過使用反射技術,很巧妙的實現了任意類型的數據導出(這裏的任意類型指的是任意的報表類型,不同的報表,導出的數據肯定是不一樣的,那麼在Java實現中的實體類肯定也是不一樣的)。要想將一個List轉換成相應的二維數組,我們得知道如下的信息:

二維數組的列數
二維數組的行數
二維數組每個元素的值
如果獲取以上三個信息呢?

通過反射中的Field[] getDeclaredFields()這個方法獲取實體類的所有字段,從而間接知道一共有多少列

List的大小不就是二維數組的行數了嘛

雖然每個實體類的字段名不一樣,那麼我們就真的無法獲取到實體類某個字段的值了嗎?不是的,你要知道,你擁有了反射,你就相當於擁有了全世界,那還有什麼做不到的呢。這裏我們沒有直接使用反射,而是使用了一個叫做BeanUtils的工具,該工具可以很方便的幫助我們對一個實體類進行字段的賦值與字段值的獲取。

很簡單,通過BeanUtils.getProperty(list.get(i), columnNames.get(j))這一行代碼,我們就獲取了實體list.get(i)中名稱爲columnNames.get(j)這個字段的值。list.get(i)當然是我們遍歷原始數據的實體類,而columnNames列表則是一個實體類所有字段名的數組,也是通過反射的方法獲取到的,具體實現可以參考LZ的源代碼。

  1. 賦值正文

這裏的正文指定是正式的表格數據內容,其實這一些沒有太多的奇淫技巧,主要的功能在上面已經實現了,這裏主要是進行單元格的賦值與導出格式的處理(主要是爲了導出excel後可以進行方便的運算)。

// 創建正文
private void createContent(Row row, CellStyle style, String[][] content, int i, Field[] fields) {
List columnNames = getBeanProperty(fields);
for (int j = 0; j < columnNames.size(); j++) {
if (formatInfo == null) {
row.createCell(j).setCellValue(content[i][j]);
continue;
}
if (formatInfo.containsKey(columnNames.get(j))) {
switch (formatInfo.get(columnNames.get(j)).getValue()) {
case “DOUBLE”:
row.createCell(j).setCellValue(Double.parseDouble(content[i][j]));
break;
case “INTEGER”:
row.createCell(j).setCellValue(Integer.parseInt(content[i][j]));
break;
case “PERCENT”:
style.setDataFormat(HSSFDataFormat.getBuiltinFormat(“0.00%”));
Cell cell = row.createCell(j);
cell.setCellStyle(style);
cell.setCellValue(Double.parseDouble(content[i][j]));
break;
case “DATE”:
row.createCell(j).setCellValue(this.parseDate(content[i][j]));
}
} else {
row.createCell(j).setCellValue(content[i][j]);
}
}
}
導出工具類的核心方法就差不多說完了,下面說一下關於多線程查詢的問題。

多扯兩點

  1. 多線程查詢數據

理想很豐滿,現實還是有點骨感的。LZ雖然對50w的數據分別創建20個線程去查詢,但是總體的效率並不是50w/20,而是僅僅快了幾秒鐘,知道原因的小夥伴可以給我留個言一起探討一下。

下面先說說具體思路:因爲多個線程之間是同時執行的,你不能夠保證哪個線程先執行完畢,但是我們卻得保證數據順序的一致性。在這裏我們使用了Callable接口,通過實現Callable接口的線程可以擁有返回值,我們獲取到所有子線程的查詢結果,然後合併到一個結果集中即可。那麼如何保證合併的順序呢?

我們先創建了一個FutureTask類型的List,該FutureTask的類型就是返回的結果集。

List<FutureTask<List>> tasks = new ArrayList<>();
當我們每啓動一個線程的時候,就將該線程的FutureTask添加到tasks列表中,這樣tasks列表中的元素順序就是我們啓動線程的順序。

FutureTask<List> task = new FutureTask<>(new listThread(map));
log.info(“開始查詢第{}條開始的{}條記錄”, i * THREAD_MAX_ROW, THREAD_MAX_ROW);
new Thread(task).start();
// 將任務添加到tasks列表中
tasks.add(task);
接下來,就是順序塞值了,我們按順序從tasks列表中取出FutureTask,然後執行FutureTask的get()方法,該方法會阻塞調用它的線程,知道拿到返回結果。這樣一套循環下來,就完成了所有數據的按順序存儲。

for (FutureTask<List> task : tasks) {
try {
productInfoPos.addAll(task.get());
} catch (Exception e) {
e.printStackTrace();
}
}
2. 如何解決接口超時

如果需要導出海量數據,可能會存在一個問題:接口超時,主要原因就是整個導出過程的時間太長了。其實也很好解決,接口的響應時間太長,我們縮短響應時間不就可以了嘛。我們使用異步編程解決方案,異步編程的實現方式有很多,這裏我們使用最簡單的spring中的Async註解,加上了這個註解的方法可以立馬返回響應結果。

關於註解的使用方式,大家可以自己查閱一下,下面講一下關鍵的實現步驟:

1、編寫異步接口,該接口負責接收客戶端的導出請求,然後開始執行導出(注意:這裏的導出不是直接向客戶端返回,而是下載到服務器本地),只要下達了導出指令,就可以馬上給客戶端返回一個該excel文件的唯一標誌(用於以後查找該文件),接口結束。

2、編寫excel狀態接口,客戶端拿到excel文件的唯一標誌之後,開始每秒輪詢調用該接口檢查excel文件的導出狀態

3、編寫從服務器本地返回excel文件接口,如果客戶端檢查到excel已經成功下載到到服務器本地,這個時候就可以請求該接口直接下載文件了。

這樣就可以解決接口超時的問題了。

源碼地址

https://github.com/dearKundy/excel-utils

源碼服用姿勢

1、建表(數據自己插入哦)

CREATE TABLE ttl_product_info (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘記錄唯一標識’,
product_name varchar(50) NOT NULL COMMENT ‘商品名稱’,
category_id bigint(20) NOT NULL DEFAULT ‘0’ COMMENT ‘類型ID’,
category_name varchar(50) NOT NULL COMMENT ‘冗餘分類名稱-避免跨表join’,
branch_id bigint(20) NOT NULL COMMENT ‘品牌ID’,
branch_name varchar(50) NOT NULL COMMENT ‘冗餘品牌名稱-避免跨表join’,
shop_id bigint(20) NOT NULL COMMENT ‘商品ID’,
shop_name varchar(50) NOT NULL COMMENT ‘冗餘商店名稱-避免跨表join’,
price decimal(10,2) NOT NULL COMMENT ‘商品當前價格-屬於熱點數據,而且價格變化需要記錄,需要價格詳情表’,
stock int(11) NOT NULL COMMENT ‘庫存-熱點數據’,
sales_num int(11) NOT NULL COMMENT ‘銷量’,
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘插入時間’,
update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘更新時間’,
is_del tinyint(3) unsigned NOT NULL DEFAULT ‘0’ COMMENT ‘記錄是否已經刪除’,
PRIMARY KEY (id),
KEY idx_shop_category_salesnum (shop_id,category_id,sales_num),
KEY idx_category_branch_price (category_id,branch_id,price),
KEY idx_productname (product_name)
) ENGINE=InnoDB AUTO_INCREMENT=15000001 DEFAULT CHARSET=utf8 COMMENT=‘商品信息表’;
2、運行程序

3、在瀏覽器的地址欄輸入:http://localhost:8080/api/excelUtils/export即可完成下載

作者:你在我家門口
原文:juejin.im/post/5c6b6b126fb9a04a0c2f024f
來源:掘金

免費分享Java技術資料,需要的朋友在關注後私信我即可免費獲取

在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章