最近寫Excel匯出功能,發現需求有點複雜,這裡整理一下思路和解決方案
一、需求背景:
老系統改造,功能和程式碼是現成的,預覽了一下內容:
第一個是有特定樣式,比如首行標題,以及紅色的列名稱
第二個,匯出多個Sheet頁
第三個,最後多一行放置匯出時間
二、技術選型 :
我非常喜歡用Hutool的工具處理,然後看了下最新的文件案例,推測是可以滿足上述需求的
http://hutool.cn/docs/#/poi/Excel生成-ExcelWriter
重點是關於如何多Sheet頁匯出的支援,Hutool這裡沒有細說,看看有沒有現成的案例
經過簡單測試發現是可行的
https://blog.csdn.net/ZLK1142/article/details/106531246/
三、落地實現:
1、前後互動問題:
本來是打算使用前端匯出的,後端介面提供資料即可,但是前端匯出怎麼設定具體樣式並不熟悉,加上自定義樣式需求多,就放棄這個方案了
使用後端匯出的基本辦法是使用get請求方式的介面,然後前端使用window.open()開啟新連結,這樣跳轉下載一個檔案
- 這樣好處是不用編寫互動處理,使用者等待新頁面彈出下載提示即可
- 但是請求引數,令牌資訊都要透過url攜帶,不安全的,也會暴露資訊
再加上現有系統無法從url上獲取引數,所以改用axios請求實現
axios請求實現的問題在於響應的處理,要在前端宣告特定的blob型別、重新封裝檔案內容、和下載事件處理
2、Hutool基於業務需求的封裝:
之前寫的匯出就是匯出資料就行,這裡參考多sheet自己實現的一個邏輯
Hutool支援了用Map配置別名對映,為了更方便實現更符合業務邏輯方式的開發,可以自定義對映註解
package cn.anmte.wd.excel.annotation; import java.lang.annotation.*; /** * @description Excel 欄位資訊 * @author OnCloud9 * @date 2024/3/6 16:26 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface ExportAlias { String value(); }
透過註解的匯出實體類,可以得到對映資訊
/** * @description 獲取匯出資料別名對映 * @author OnCloud9 * @date 2024/3/6 16:16 * @params * @return */ public static <ExportDTO> Map<String, String> getExportDataAlias(Class<ExportDTO> exportClass) { Map<String, String> aliasMap = new HashMap<>(); Field[] declaredFields = exportClass.getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); ExportAlias annotation = field.getAnnotation(ExportAlias.class); if (Objects.isNull(annotation)) continue; aliasMap.put(name, annotation.value()); } return aliasMap; }
因為是多sheet,所以要宣告一個類封裝sheet資訊
package cn.anmte.wd.excel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @Data @AllArgsConstructor @NoArgsConstructor @Builder public class SheetInfo<ExportData> { private String sheetName; private String sheetTitle; /* 匯出實體的class物件 */ private Class<ExportData> exportDataClass; /* 匯出的資料 */ private List<ExportData> exportDataList; /* 別名對映過濾方法 預設不處理 */ private Function<Map<String, String>, Map<String, String>> aliasFilter; /* 資料寫入完成後的操作 -> 預設執行內容 */ private Consumer<WdImcExcelUtil.AfterWrite> awConsumer; }
存在動態列名匯出的場景,這裡基於解析對映資訊的基礎上,追加了調整對映資訊的方法:
提供一個Function方法介面,投入解析好的對映Map,具體調整方法交給外部呼叫實現
private static Map<String, String> aliasConfig(ExcelWriter writer, SheetInfo<?> sheetInfo) { Map<String, String> aliasMap = getExportDataAlias(sheetInfo.getExportDataClass()); Function<Map<String, String>, Map<String, String>> aliasFilter = sheetInfo.getAliasFilter(); if (Objects.nonNull(aliasFilter)) aliasMap = aliasFilter.apply(aliasMap); writer.clearHeaderAlias(); writer.setHeaderAlias(aliasMap); writer.setOnlyAlias(true); return aliasMap; }
WebServlet下載邏輯部分:
private static void exportForDownload(HttpServletResponse response, ExcelWriter writer, String workBookName) { ServletOutputStream out = null; try { response.setContentType("application/vnd.ms-excel;charset=utf-8"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(workBookName + ".xlsx", "UTF-8")); out = response.getOutputStream(); writer.flush(out, true); } catch (IOException e) { e.printStackTrace(); } finally { // 關閉writer,釋放記憶體 writer.close(); } // 關閉輸出Servlet流 IoUtil.close(out); }
匯出整裝的邏輯:
後置處理和上面的動態對映也是同一個邏輯,外部實現
因為我想把那些樣式處理放到這裡統一執行,邏輯層次是清晰的,易於維護
在下面匯出可以再補充其他下載方式,開發時間有限就寫到這個程度了
/** * @description * @author Cloud9 * @date 2024/3/6 17:02 * @params * @return */ public static void writeWdMultiSheetWorkBook(HttpServletResponse response, String workBookName, List<SheetInfo<?>> sheetInfoList) { if (CollectionUtils.isEmpty(sheetInfoList)) return; ExcelWriter writer = ExcelUtil.getWriter(); /* 開啟多sheet頁支援方法 */ writer.renameSheet(0, sheetInfoList.get(0).getSheetName()); sheetInfoList.forEach(sheetInfo -> { /* sheet名稱設定 */ writer.setSheet(sheetInfo.getSheetName()); /* sheet別名對映設定 */ Map<String, String> aliasMap = aliasConfig(writer, sheetInfo); /* 設定標頭內容 */ if(StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1, sheetInfo.getSheetTitle()); /* 寫入資料 */ writer.write(sheetInfo.getExportDataList(), true); /* 後置處理 */ Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer(); if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build()); }); exportForDownload(response, writer, workBookName); }
完整工具類程式碼(WdImcExcelUtil):
這裡樣式設定的程式碼沒完全寫好,可以提供參考
package cn.anmte.wd.excel; import cn.anmte.wd.excel.annotation.ExportAlias; import cn.hutool.core.io.IoUtil; import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.poi.excel.ExcelWriter; import cn.hutool.poi.excel.style.StyleUtil; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.usermodel.*; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Field; import java.net.URLEncoder; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Function; /** * @description * https://www.hutool.cn/docs/#/poi/Excel%E7%94%9F%E6%88%90-ExcelWriter * @author OnCloud9 * @date 2024/3/6 16:11 */ public class WdImcExcelUtil { /** * @description 獲取當前時間線字尾 * @author OnCloud9 * @date 2024/3/6 17:13 * @params * @return */ public static String getCurrentTimeSuffix(String format) { if (StringUtils.isBlank(format)) format = "yyyyMMddHHmmss"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); return LocalDateTime.now().format(formatter); } /** * @description * @author OnCloud9 * @date 2024/3/6 17:33 * @params * @return */ public static void writeWdSheetWorkBook(HttpServletResponse response, String workBookName, SheetInfo<?> sheetInfo) { ExcelWriter writer = ExcelUtil.getWriter(); writer.renameSheet(0, sheetInfo.getSheetName()); writer.setSheet(sheetInfo.getSheetName()); /* sheet別名對映設定 */ Map<String, String> aliasMap = aliasConfig(writer, sheetInfo); /* 設定標頭內容 */ if(StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1, sheetInfo.getSheetTitle()); /* 寫入資料 */ writer.write(sheetInfo.getExportDataList(), true); /* 後置處理 */ Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer(); if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build()); exportForDownload(response, writer, workBookName); } /** * @description * @author OnCloud9 * @date 2024/3/6 17:02 * @params * @return */ public static void writeWdMultiSheetWorkBook(HttpServletResponse response, String workBookName, List<SheetInfo<?>> sheetInfoList) { if (CollectionUtils.isEmpty(sheetInfoList)) return; ExcelWriter writer = ExcelUtil.getWriter(); /* 開啟多sheet頁支援方法 */ writer.renameSheet(0, sheetInfoList.get(0).getSheetName()); sheetInfoList.forEach(sheetInfo -> { /* sheet名稱設定 */ writer.setSheet(sheetInfo.getSheetName()); /* sheet別名對映設定 */ Map<String, String> aliasMap = aliasConfig(writer, sheetInfo); /* 設定標頭內容 */ if(StringUtils.isNotBlank(sheetInfo.getSheetTitle())) writer.merge(aliasMap.size() - 1, sheetInfo.getSheetTitle()); /* 寫入資料 */ writer.write(sheetInfo.getExportDataList(), true); /* 後置處理 */ Consumer<AfterWrite> awConsumer = sheetInfo.getAwConsumer(); if (Objects.nonNull(awConsumer)) awConsumer.accept(AfterWrite.builder().writer(writer).aliasMap(aliasMap).build()); }); exportForDownload(response, writer, workBookName); } private static Map<String, String> aliasConfig(ExcelWriter writer, SheetInfo<?> sheetInfo) { Map<String, String> aliasMap = getExportDataAlias(sheetInfo.getExportDataClass()); Function<Map<String, String>, Map<String, String>> aliasFilter = sheetInfo.getAliasFilter(); if (Objects.nonNull(aliasFilter)) aliasMap = aliasFilter.apply(aliasMap); writer.clearHeaderAlias(); writer.setHeaderAlias(aliasMap); writer.setOnlyAlias(true); return aliasMap; } private static void exportForDownload(HttpServletResponse response, ExcelWriter writer, String workBookName) { ServletOutputStream out = null; try { response.setContentType("application/vnd.ms-excel;charset=utf-8"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(workBookName + ".xlsx", "UTF-8")); out = response.getOutputStream(); writer.flush(out, true); } catch (IOException e) { e.printStackTrace(); } finally { // 關閉writer,釋放記憶體 writer.close(); } // 關閉輸出Servlet流 IoUtil.close(out); } /** * @description 獲取匯出資料別名對映 * @author OnCloud9 * @date 2024/3/6 16:16 * @params * @return */ public static <ExportDTO> Map<String, String> getExportDataAlias(ExportDTO dto) { Map<String, String> aliasMap = new HashMap<>(); Class<?> exportClass = dto.getClass(); Field[] declaredFields = exportClass.getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); ExportAlias annotation = field.getAnnotation(ExportAlias.class); if (Objects.isNull(annotation)) continue; aliasMap.put(name, annotation.value()); } return aliasMap; } /** * @description 獲取匯出資料別名對映 * @author OnCloud9 * @date 2024/3/6 16:16 * @params * @return */ public static <ExportDTO> Map<String, String> getExportDataAlias(Class<ExportDTO> exportClass) { Map<String, String> aliasMap = new HashMap<>(); Field[] declaredFields = exportClass.getDeclaredFields(); for (Field field : declaredFields) { String name = field.getName(); ExportAlias annotation = field.getAnnotation(ExportAlias.class); if (Objects.isNull(annotation)) continue; aliasMap.put(name, annotation.value()); } return aliasMap; } /** * @description 頭部樣式設定 * @author OnCloud9 * @date 2024/3/6 16:08 * @params * @return */ public static void headerStyleSetting(ExcelWriter excelWriter) { CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook()); Sheet sheet = excelWriter.getSheet(); Row row = sheet.getRow(0); /* 設定單元行高度為50磅 */ row.setHeight((short) 1000); /* 建立頭部樣式的自定義字型 */ Font font = excelWriter.createFont(); font.setFontName("Arial"); font.setBold(true); font.setFontHeightInPoints((short) 24); /* 設定預設的背景色 */ cellStyle.setFont(font); cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cellStyle.setAlignment(HorizontalAlignment.CENTER); // 設定水平居中 cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 設定垂直居中 /* 放置樣式 */ Cell cell = row.getCell(0); cell.setCellStyle(cellStyle); } /** * @description 列名樣式設定 * @author OnCloud9 * @date 2024/3/6 16:08 * @params * @return */ public static void columnNameStyleSetting(ExcelWriter excelWriter, int colSize) { CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook()); Sheet sheet = excelWriter.getSheet(); Row row = sheet.getRow(1); /* 建立頭部樣式的自定義字型 */ Font font = excelWriter.createFont(); font.setFontName("Arial"); font.setBold(true); font.setColor(Font.COLOR_RED); font.setFontHeightInPoints((short) 10); /* 設定樣式 */ cellStyle.setFont(font); cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); /* 邊框樣式 */ // 建立邊框物件,並設定邊框樣式 BorderStyle borderStyle = BorderStyle.THIN; // 細邊框 short blackIndex = IndexedColors.BLACK.getIndex(); cellStyle.setBorderTop(borderStyle); // 設定上邊框 cellStyle.setTopBorderColor(blackIndex); // 設定邊框顏色為黑色 cellStyle.setBorderBottom(borderStyle); // 設定下邊框 cellStyle.setBottomBorderColor(blackIndex); cellStyle.setBorderLeft(borderStyle); // 設定左邊框 cellStyle.setLeftBorderColor(blackIndex); cellStyle.setBorderRight(borderStyle); // 設定右邊框 cellStyle.setRightBorderColor(blackIndex); for (int i = 0; i < colSize; i++) { Cell cell = row.getCell(i); cell.setCellStyle(cellStyle); } } /** * @description * @author OnCloud9 * @date 2024/3/7 17:37 * @params * @return */ public static void timelineMark(ExcelWriter excelWriter, int rowIdx, int colIdx) { CellStyle cellStyle = StyleUtil.createCellStyle(excelWriter.getWorkbook()); String currentTime = getCurrentTimeSuffix("yyyy-MM-dd HH:mm:ss"); excelWriter.setCurrentRow(rowIdx); excelWriter.merge(colIdx, "時間:" + currentTime); Cell cell = excelWriter.getOrCreateCell(rowIdx, 0); /* 設定右居中 */ cellStyle.setAlignment(HorizontalAlignment.RIGHT); // 設定右居中 cellStyle.setVerticalAlignment(VerticalAlignment.CENTER); // 設定垂直居中 cellStyle.setFillForegroundColor(IndexedColors.WHITE.getIndex()); cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); cell.setCellStyle(cellStyle); } @Data @AllArgsConstructor @NoArgsConstructor @Builder public static class AfterWrite { private ExcelWriter writer; private Map<String, String> aliasMap; } }
3、前端JS部分:
封裝好axios處理
import { CUSTOMAPIURl } from '@/utils/define' import axios from 'axios' /** * @param apiPath 請求路徑 * @param postData 請求引數 * @param token 令牌 * @param fileName 檔名 * @param whenDone 完成時回撥 * @param whenErr 異常時回撥 */ export function requestExcelExport({ apiPath, postData, token, fileName, whenDone, whenErr }) { axios({ method: 'post', url: CUSTOMAPIURl + apiPath, data: postData, responseType: 'blob', headers: { 'Content-Type': 'application/json', // 示例的 header,你可以根據需要新增更多 'Authorization': token// 示例的授權 header } }).then(function (response) { // 建立一個 blob URL const blobUrl = window.URL.createObjectURL(new Blob([response.data])) const link = document.createElement('a') link.href = blobUrl; link.setAttribute('download', fileName); // 設定下載檔案的名稱 document.body.appendChild(link) // 觸發點選事件來下載檔案 link.click(); // 清理 window.URL.revokeObjectURL(blobUrl); document.body.removeChild(link); whenDone() }).catch(function (error) { // 請求失敗後的處理 console.error('Error downloading Excel file:', error); whenErr() }) }
方法使用:
四、實現效果:
這裡還沒處理對映順序,看起來有點亂,等後面我再追加補充吧....