首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

SSH 框架整合 POI 導出複雜 Excel:多 Sheet、合併儲存格、樣式與下載全流程

     編輯:Java 綜合教學

企業級報表導出實戰:從數據庫查詢到瀏覽器下載的完整鏈路

本文記錄一個真實需求:在 SSH(Struts2+Spring+Hibernate)框架下,使用 Apache POI 導出具有固定複雜格式的 Excel 文件——多 Sheet(按變電站分)、合併儲存格、多層標題、序號、自定義樣式、底部備註區——並實現前端參數傳遞、後端流式下載。包含踩坑總結與完整代碼。

一、需求背景

  • 頁面按「變電站、電壓等級」查詢定值單列表
  • 勾選行後點擊「生成三核對文件」
  • 導出 Excel:每個變電站一個 Sheet,包含標題、表頭、數據體、合併儲存格的核對簽名區、底部固定備註
  • 瀏覽器彈出下載對話框

需求原型圖

二、核心難點與解決方案

1. 數據庫查詢:游標解決「同一 GUID 多行轉單行」

業務表中「執行人/執行日期」與「核對人/核對日期」分散在不同行,需橫向合併。使用 SQL Server 游標(DECLARE CUR CURSOR FORWARD_ONLY)順序讀取兩行,賦值給變量 @NAME2~@NAME6,再在主查詢中拼接:

DECLARE @NAME2 NVARCHAR(300), @NAME3 NVARCHAR(300), @NAME5 NVARCHAR(300), @NAME6 NVARCHAR(300);
DECLARE CUR CURSOR FORWARD_ONLY FOR
  SELECT TASKACTOR, ENDTIME FROM US_SYS.TV_WF_TASKLOG
  WHERE PROCESSINSTANCE = (SELECT PROCESSINSTANCE_ FROM US_SYS.JBPM_VARIABLEINSTANCE WHERE STRINGVALUE_ = ? AND NAME_ = 'orderNo')
    AND (TASKNAME LIKE '執行' OR TASKNAME LIKE '核對') AND ENDTIME IS NOT NULL;
OPEN CUR;
FETCH NEXT FROM CUR INTO @NAME2, @NAME3;  -- 第一行:執行
FETCH NEXT FROM CUR INTO @NAME5, @NAME6;  -- 第二行:核對
CLOSE CUR; DEALLOCATE CUR;

SELECT ..., 
  CAST(@NAME2 AS VARCHAR) + CAST(@NAME3 AS VARCHAR) AS ENDTIME1,  -- 執行人+日期
  CAST(@NAME5 AS VARCHAR) + CAST(@NAME6 AS VARCHAR) AS ENDTIME2   -- 核對人+日期
FROM ...

2. GUID 列表轉 IN 子句字符串

String guidInSql = "'" + guid.replaceAll(",", "','") + "'"; // 'guid1','guid2',...

3. 分 Sheet 策略:先查變電站列表,再循環查詢每站數據

List<String> subNames = getSubNames(guidInSql); // SELECT DISTINCT SUBSTATION ...
for (String sub : subNames) {
    String[] guids = getGuidsBySubstation(sub, guidInSql);
    DataTable dt = queryDetail(guids); // 含游標邏輯
    buildSheet(workbook, sub, dt);
}

4. POI 核心操作:合併儲存格、樣式、序號

合併儲存格四步法:

// 1. 定義區域 (firstRow, lastRow, firstCol, lastCol)
CellRangeAddress region = new CellRangeAddress(0, 0, 0, 12);
// 2. 註冊到 Sheet
sheet.addMergedRegion(region);
// 3. 獲取/創建左上角儲存格
Row row = sheet.createRow(0);
Cell cell = row.createCell(0);
// 4. 賦值 & 設置樣式
cell.setCellValue("台州電網繼電保護及安全自動裝置整定單核對表");
cell.setCellStyle(titleStyle);

樣式復用原則:創建 CellStyle、Font 對象一次,全局共享,避免超過 64k 樣式限制導致文件損壞。

5. 前後端參數傳遞:Form POST 模擬下載

AJAX 無法直接處理二進制流下載,採用動態 Form 提交:

// 前端 JS
function openPostWindow(url, params) {
    var form = $('
'); $.each(params, function(name, value) { form.append($('')); }); $('body').append(form); form.submit(); form.remove(); } // 調用 var params = { substationaa: '白雲變', voltageaa: '220kV', guid: 'g1,g2,g3' }; openPostWindow('checkManagement/cdzdExcel/importsExcel.do', params);

後端 Spring MVC 以 @RequestParam 接收:

@RequestMapping("importsExcel.do")
@ResponseBody
public String cdzdExcel(@RequestParam(value="substationaa", required=false) String substationaa,
                        @RequestParam(value="voltageaa", required=false) String voltageaa,
                        @RequestParam(value="guid", required=false) String guid) { ... }

6. 流式下載響應:雙重寫入(可選保存服務器副本)

HSSFWorkbook workbook = service.getExcel(guids, subNames, userModel);

HttpServletResponse response = (HttpServletResponse) ThreadLocalUtils.getObjectFromThreadLocal("response");
response.reset();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/x-msdownload");
String fileName = year + "年" + voltage + substation + "變電站定值單三核對表.xls";
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));

// 可選:保存服務器副本
FileOutputStream fileOut = new FileOutputStream("c:/" + fileName);
workbook.write(fileOut);
fileOut.close();

// 瀏覽器下載流
OutputStream out = response.getOutputStream();
workbook.write(out);
out.flush();
out.close();

三、完整代碼結構

Controller 層

@Controller
@RequestMapping("/cdzdExcel")
public class CdzdsuperImportExcelController {
    @Resource(name="CdzdsuperImportExcelServiceImpl")
    private CdzdsuperImportExcelServiceImpl service;
    private JsonBinder json = JsonBinder.getJsonBinder();

    @RequestMapping("importsExcel.do")
    @ResponseBody
    public String cdzdExcel(@RequestParam(value="substationaa",required=false) String substationaa,
                            @RequestParam(value="voltageaa",required=false) String voltageaa,
                            @RequestParam(value="guid",required=false) String guid) {
        // 1. 解析 GUID 數組、構建 IN 子句
        // 2. 查詢變電站列表 subNames
        // 3. 生成 Workbook
        HSSFWorkbook workbook = service.getExcel(guids, subNames, userModel);
        // 4. 響應下載(見上文)
        return json.toJson(new ValidateMessage(true, "success"));
    }
}

Service 層核心邏輯

@Service("CdzdsuperImportExcelServiceImpl")
public class CdzdsuperImportExcelServiceImpl {
    @Resource GenericDao genericDao;

    public HSSFWorkbook getExcel(String[] guids, String[] subNames, UserModel user) {
        HSSFWorkbook wb = POIExcelUtil.createNewHSSFWorkbook();
        for (int i=0; i DataTable
            DataTable dt = queryDetail(guids, subNames[i]);
            // 構建 Sheet
            buildSheet(wb, subNames[i], dt);
        }
        return wb;
    }

    private void buildSheet(HSSFWorkbook wb, String sheetName, DataTable dt) {
        HSSFSheet sheet = wb.createSheet(sheetName);
        // 設置列寬、標題樣式、表頭樣式、數據樣式...
        // 寫入標題行(合併 0-12 列)
        // 寫入表頭(第 2 行序號,第 3 行列名)
        // 循環寫入數據行
        // 寫入底部合併區域(核對簽名、備註)
    }

    private DataTable queryDetail(String[] guids, String subName) {
        // 構建含游標的 SQL,調用 genericDao.getDataIntoDataTable
        // 返回單站 DataTable
    }

    public List getSubNames(String guidInSql) { ... }
}

四、效果展示

導出效果圖

五、踩坑總結與心得

  1. 數據庫游標:解決橫向合併行數據的關鍵,但調試困難,建議先在 SSMS 驗證 SQL。
  2. GUID 長度截斷:JDBC 默認字符長度可能截斷 GUID,需在 SQL 中 CAST(GUID AS VARCHAR(500))。
  3. 前後端調試:debugger 斷點 + Network 面板查看 Form Data,排查 400 錯誤(多為參數名不匹配)。
  4. POI 樣式:統一創建樣式對象,避免重複創建導致文件損壞。
  5. 心態:遇到不會的技術點,先 Debug 理解現狀,再查資料/問人,切勿盲目試錯(作者曾建 100+ 臨時文件)。

六、參考資源

完整代碼已開源於項目倉庫,歡迎交流指正。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved