公司項目最近有一個須要:報表導出。整個系統下來,起碼超過一百張報表須要導出。這個時候如何優雅的實現報表導出,釋放生產力就顯得很重要了。下面主要給你們分享一下該工具類的使用方法與實現思路。java
對於每一個報表都相同的操做,咱們很天然的會抽離出來,這個很簡單。而最重要的是:如何把那些每一個報表不相同的操做進行良好的封裝,儘量的提升複用性;針對以上的原則,主要實現了一下關鍵功能點:git
上面說到了本工具類實現了三個功能點,天然在使用的時候設置好這三個要點便可:github
下面的export
函數能夠直接向客戶端返回一個excel數據,其中productInfoPos
爲待導出的數據列表,ExcelHeaderInfo
用來保存表頭信息,包括表頭名稱,表頭的首列,尾列,首行,尾行。由於默認導出的數據格式都是字符串型,因此還須要一個Map參數用來指定某個字段的格式化類型(例如數字類型,小數類型、日期類型)。這裏你們知道個大概怎麼使用就行了,下面會對這些參數進行詳細解釋。spring
@Override
public void export(HttpServletResponse response, String fileName) {
// 待導出數據
List<TtlProductInfoPo> productInfoPos = this.multiThreadListProduct();
ExcelUtils excelUtils = new ExcelUtils(productInfoPos, getHeaderInfo(), getFormatInfo());
excelUtils.sendHttpResponse(response, fileName, excelUtils.getWorkbook());
}
// 獲取表頭信息
private List<ExcelHeaderInfo> 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;
}
複製代碼
哈哈,本身分析本身的代碼,有點意思。因爲不方便貼出太多的代碼,你們能夠先到github上clone源碼,再回來閱讀文章。✨源碼地址✨ LZ使用的poi 4.0.1
版本的這個工具,想要實用海量數據的導出天然得使用SXSSFWorkbook
這個組件。關於poi的具體用法在這裏我就很少說了,這裏主要是給你們講解如何對poi進行封裝使用。sql
咱們重點看ExcelUtils
這個類,這個類是實現導出的核心,先來看一下三個成員變量。編程
private List list;
private List<ExcelHeaderInfo> excelHeaderInfos;
private Map<String, ExcelFormat> formatInfo;
複製代碼
該成員變量用來保存待導出的數據。api
該成員變量主要用來保存表頭信息,由於咱們須要定義多個表頭信息,因此須要使用一個列表來保存,ExcelHeaderInfo
構造函數以下 ExcelHeaderInfo(int firstRow, int lastRow, int firstCol, int lastCol, String title)
數組
firstRow
:該表頭所佔位置的首行lastRow
:該表頭所佔位置的尾行firstCol
:該表頭所佔位置的首列lastCol
:該表頭所佔位置的尾行title
:該表頭的名稱該參數主要用來格式化字段,咱們須要預先約定好轉換成那種格式,不能隨用戶本身定。因此咱們定義了一個枚舉類型的變量,該枚舉類只有一個字符串類型成員變量,用來保存想要轉換的格式,例如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;
}
}
複製代碼
該方法用來初始化表頭,而建立表頭最關鍵的就是poi中Sheet類的
addMergedRegion(CellRangeAddress var1)
方法,該方法用於單元格融合
。咱們會遍歷ExcelHeaderInfo列表,按照每一個ExcelHeaderInfo的座標信息進行單元格融合,而後在融合以後的每一個單元首行
和首列
的位置建立單元格,而後爲單元格賦值便可,經過上面的步驟就完成了任意類型的表頭設置。bash
// 建立表頭
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);
}
}
複製代碼
在進行正文賦值以前,咱們先要對原始數據列表轉換成字符串的二維數組,之因此轉成字符串格式是由於能夠統一的處理各類類型,以後有須要咱們再轉換回來便可。
// 將原始數據轉成二維數組
private String[][] transformData() {
int dataSize = this.list.size();
String[][] datas = new String[dataSize][];
// 獲取報表的列數
Field[] fields = list.get(0).getClass().getDeclaredFields();
// 獲取實體類的字段名稱數組
List<String> 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()
這個方法獲取實體類的全部字段,從而間接知道一共有多少列反射
,你就至關於擁有了全世界,那還有什麼作不到的呢。這裏咱們沒有直接使用反射,而是使用了一個叫作BeanUtils
的工具,該工具能夠很方便的幫助咱們對一個實體類進行字段的賦值與字段值的獲取。很簡單,經過BeanUtils.getProperty(list.get(i), columnNames.get(j))
這一行代碼,咱們就獲取了實體list.get(i)
中名稱爲columnNames.get(j)
這個字段的值。list.get(i)
固然是咱們遍歷原始數據的實體類,而columnNames
列表則是一個實體類全部字段名的數組,也是經過反射的方法獲取到的,具體實現能夠參考LZ的源代碼。這裏的正文指定是正式的表格數據內容,其實這一些沒有太多的奇淫技巧,主要的功能在上面已經實現了,這裏主要是進行單元格的賦值與導出格式的處理(主要是爲了導出excel後能夠進行方便的運算)。
// 建立正文
private void createContent(Row row, CellStyle style, String[][] content, int i, Field[] fields) {
List<String> 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]);
}
}
}
複製代碼
導出工具類的核心方法就差很少說完了,下面說一下關於多線程查詢的問題。
理想很豐滿,現實仍是有點骨感的。LZ雖然對50w的數據分別建立20個線程去查詢,可是整體的效率並非50w/20,而是僅僅快了幾秒鐘,知道緣由的小夥伴能夠給我留個言一塊兒探討一下。
下面先說說具體思路:由於多個線程之間是同時執行的,你不可以保證哪一個線程先執行完畢,可是咱們卻得保證數據順序的一致性。在這裏咱們使用了Callable
接口,經過實現Callable
接口的線程能夠擁有返回值,咱們獲取到全部子線程的查詢結果,而後合併到一個結果集中便可。那麼如何保證合併的順序呢?咱們先建立了一個FutureTask
類型的List,該FutureTask
的類型就是返回的結果集。
List<FutureTask<List<TtlProductInfoPo>>> tasks = new ArrayList<>();
複製代碼
當咱們每啓動一個線程的時候,就將該線程的FutureTask
添加到tasks
列表中,這樣tasks列表中的元素順序就是咱們啓動線程的順序。
FutureTask<List<TtlProductInfoPo>> 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<TtlProductInfoPo>> task : tasks) {
try {
productInfoPos.addAll(task.get());
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
若是須要導出海量數據,可能會存在一個問題:接口超時
,主要緣由就是整個導出過程的時間太長了。其實也很好解決,接口的響應時間太長,咱們縮短響應時間不就能夠了嘛。咱們使用異步編程
解決方案,異步編程的實現方式有不少,這裏咱們使用最簡單的spring中的Async
註解,加上了這個註解的方法能夠立馬返回響應結果。關於註解的使用方式,你們能夠本身查閱一下,下面講一下關鍵的實現步驟:
到服務器本地
,這個時候就能夠請求該接口直接下載文件了。這樣就能夠解決接口超時的問題了。
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='商品信息表';
複製代碼
本次文章就寫到這裏啦,喜歡的朋友能夠點贊、評論、加關注哦!