在企業級報表導出場景中,Apache POI 是處理 Excel(.xls/.xlsx)的標準選擇。本文針對「行/列可能為空」導致的 NullPointerException 高發問題,提供基於 createRow/createCell 的動態構建方案,並分享類型安全取值與 Spring MVC 下載全鏈路代碼。
<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>
錯誤示範:
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 都會覆蓋或新建,徹底消除空指針。
從 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
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);
}
}
}
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();
}
this.getClass().getResource("/resource/template.xls") 需確保資源文件打包進 JAR(放在 src/main/resources)。SXSSFWorkbook(流式寫入),避免 OOM。CellStyle 對象一次,多單元格復用,防止樣式對象過多導致文件損壞。CreationHelper.createDataFormat() 設置日期格式;公式需 cell.setCellFormula("A1+B1")。掌握動態創建行列、空值安全轉型、流式下載三大核心,即可應對 90% 以上 Excel 導出業務場景。