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

Java POI 讀寫 Excel 實戰:優雅處理空行與空單元格

     編輯:Java 綜合教學

Apache POI 操作 Excel 核心技巧:動態創建行/列與空值安全

在企業級報表導出場景中,Apache POI 是處理 Excel(.xls/.xlsx)的標準選擇。本文針對「行/列可能為空」導致的 NullPointerException 高發問題,提供基於 createRow/createCell 的動態構建方案,並分享類型安全取值與 Spring MVC 下載全鏈路代碼。

一、Maven 依賴

<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi</artifactId>
  <version>4.1.2</version> <!-- 或更新版本 -->
</dependency>
<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-ooxml</artifactId>
  <version>4.1.2</version>
</dependency>

二、核心原則:拒絕 getRow/getCell,擁抱 createRow/createCell

錯誤示範:

Row row = sheet.getRow(i); // 若行不存在返回 null
Cell cell = row.getCell(j); // 若列不存在返回 null
cell.setCellValue("value"); // NPE 風險極高

正確姿勢:

Row row = sheet.createRow(i); // 確保行存在
Cell cell = row.createCell(j); // 確保列存在
cell.setCellValue("value"); // 安全賦值
無論模板是否預置數據,createRow/createCell 都會覆蓋或新建,徹底消除空指針。

三、類型安全取值:強制轉型優於 toString()

從 Map<String, Object> 取值時:

// 推薦:直接強轉,null 安全
String name = (String) map.get("NAME"); // null 不拋異常

// 不推薦:toString() 需非 null
String name = map.get("NAME").toString(); // null 會拋 NPE

// 數值類型需顯式轉換
BigDecimal price = (BigDecimal) map.get("PRICE");
if (price != null) {
    cell.setCellValue(price.doubleValue());
}

四、完整導出服務實現 (Service 層)

@Service
public class MatYearPlanExportService {

    public InputStream exportExcel(Map<String, Object> params, Map<String, Object> operator) {
        try (NPOIFSFileSystem fs = new NPOIFSFileSystem(new FileInputStream(templatePath));
             Workbook workbook = new HSSFWorkbook(fs.getRoot(), true);
             ByteArrayOutputStream baos = new ByteArrayOutputStream()) {

            Sheet sheet = workbook.getSheetAt(0);
            List<Map<String, Object>> dataList = queryData(params, operator);

            for (int i = 0; i < dataList.size(); i++) {
                Map<String, Object> rowData = dataList.get(i);
                Row row = sheet.createRow(i + 1); // 跳過標題行

                // 字符串字段
                row.createCell(0).setCellValue((String) rowData.get("MAT_NO_"));
                row.createCell(1).setCellValue((String) rowData.get("MAT_DESC_"));

                // 數值字段(含空值保護)
                BigDecimal plannedPrice = (BigDecimal) rowData.get("PLANNED_PRICE_");
                if (plannedPrice != null) {
                    row.createCell(4).setCellValue(plannedPrice.doubleValue());
                }

                // 字典碼轉換(示例:物料類型)
                BigDecimal matType = (BigDecimal) rowData.get("MAT_TYPE_");
                if (matType != null) {
                    String typeName = dictService.getDictValue("MAT_TYPE_", matType.intValue(), operator);
                    row.createCell(7).setCellValue(typeName);
                }

                // 樹形結構遞歸拼接(預算分類全路徑)
                String budgetCatId = (String) rowData.get("MAT_BUDGET_CAT_ID_");
                if (budgetCatId != null) {
                    String fullPath = buildBudgetCatPath(budgetCatId, operator);
                    row.createCell(9).setCellValue("/" + fullPath + "/");
                }
            }

            workbook.write(baos);
            return new ByteArrayInputStream(baos.toByteArray());
        } catch (IOException e) {
            log.error("Excel 導出失敗", e);
            throw new BizException("導出異常");
        }
    }

    private String buildBudgetCatPath(String catId, Map<String, Object> operator) {
        // 遞歸查詢父節點拼接路徑,略
        return "";
    }
}

五、控制器層:流式下載響應

@Controller
@RequestMapping("/export")
public class ExportController {

    @Resource
    private MatYearPlanExportService exportService;

    @GetMapping("/matYearPlan")
    public void downloadMatYearPlan(HttpServletRequest request,
                                    HttpServletResponse response,
                                    HttpSession session,
                                    @RequestParam Map<String, Object> params) {
        try {
            Map<String, Object> operator = getOperator(session);
            InputStream is = exportService.exportExcel(params, operator);

            response.reset();
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/vnd.ms-excel");
            String fileName = URLEncoder.encode("廠礦年計劃表.xls", "UTF-8");
            response.setHeader("Content-Disposition", "attachment; filename=" + fileName);

            try (ServletOutputStream out = response.getOutputStream()) {
                byte[] buffer = new byte[8192];
                int len;
                while ((len = is.read(buffer)) != -1) {
                    out.write(buffer, 0, len);
                }
                out.flush();
            }
        } catch (Exception e) {
            log.error("下載失敗", e);
        }
    }
}

六、前端調用 (ExtJS 示例)

function downloadExcel() {
    var params = {
        YEAR_: Ext.getCmp('YEAR_').getValue(),
        MONTH_: Ext.getCmp('MONTH_').getValue(),
        // ... 其他查詢條件
    };
    // 表單 POST 觸發下載,避免 AJAX 二進制流處理麻煩
    var form = document.createElement('form');
    form.action = '/export/matYearPlan';
    form.method = 'POST';
    form.target = '_blank';
    Object.keys(params).forEach(key => {
        var input = document.createElement('input');
        input.type = 'hidden';
        input.name = key;
        input.value = params[key];
        form.appendChild(input);
    });
    document.body.appendChild(form);
    form.submit();
    form.remove();
}

七、常見坑點總結

  1. 模板路徑:this.getClass().getResource("/resource/template.xls") 需確保資源文件打包進 JAR(放在 src/main/resources)。
  2. 大數據量:超過 65535 行需切換 SXSSFWorkbook(流式寫入),避免 OOM。
  3. 樣式複用:創建 CellStyle 對象一次,多單元格復用,防止樣式對象過多導致文件損壞。
  4. 日期/公式:使用 CreationHelper.createDataFormat() 設置日期格式;公式需 cell.setCellFormula("A1+B1")。

八、參考資源

掌握動態創建行列、空值安全轉型、流式下載三大核心,即可應對 90% 以上 Excel 導出業務場景。

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