Apache POI 是 Java 操作 Microsoft Office 文件的事實標準庫。本文示範如何解析 .xls (HSSF) 與 .xlsx (XSSF) 雙格式,針對多 Sheet、指定欄位提取、資料轉換與異常處理提供生產級範本。
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>5.3.0</version> <!-- 請檢查最新版 -->
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.3.0</version>
</dependency>
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class ExcelParser {
/** 統一入口:自動偵測格式並委派解析 */
public List<MeetBean> parse(String level, String filePath) {
try (InputStream is = new FileInputStream(filePath)) {
Workbook workbook = createWorkbook(filePath, is);
if (workbook == null) return List.of();
// 依 level 決定解析策略(Sheet 索引、起始行、欄位映射)
ParseStrategy strategy = ParseStrategy.fromLevel(level);
return parseSheet(workbook, strategy);
} catch (Exception e) {
throw new RuntimeException("Excel 解析失敗: " + filePath, e);
}
}
/** 工廠方法:建立對應 Workbook 實例 */
private Workbook createWorkbook(String filePath, InputStream is) throws IOException {
if (filePath.endsWith(".xls")) return new HSSFWorkbook(is);
if (filePath.endsWith(".xlsx")) return new XSSFWorkbook(is);
return null;
}
/** 通用 Sheet 解析邏輯 */
private List<MeetBean> parseSheet(Workbook wb, ParseStrategy s) {
Sheet sheet = wb.getSheetAt(s.sheetIndex);
int lastRow = sheet.getLastRowNum();
List<MeetBean> result = new ArrayList<>();
for (int i = s.startRow; i <= lastRow; i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
Cell nameCell = row.getCell(s.nameCol);
Cell hintCell = row.getCell(s.hintCol);
if (nameCell == null || hintCell == null) continue;
String name = getStringCellValue(nameCell);
if (name.isEmpty()) break; // 遇空名稱視為結束
MeetBean bean = new MeetBean();
bean.setMeetid("YD" + i);
bean.setMeetname(name);
bean.setMeethint(getStringCellValue(hintCell));
bean.setMeettime(generateRandomTime()); // 業務邏輯:隨機時間
bean.setMeetLevel(s.level);
result.add(bean);
}
return result;
}
/** 安全取得字串值(支援數值/布林/公式轉字串) */
private String getStringCellValue(Cell cell) {
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue().trim();
case NUMERIC -> DateUtil.isCellDateFormatted(cell)
? cell.getDateCellValue().toString()
: String.valueOf((long) cell.getNumericCellValue());
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
case FORMULA -> cell.getCellFormula();
default -> "";
};
}
/** 策略枚舉:封裝各 level 差異配置 */
private enum ParseStrategy {
LEVEL_401("401", 0, 3, 2, 4),
LEVEL_402("402", 1, 3, 2, 4),
LEVEL_403("403", 2, 3, 1, 2); // 注意:原碼 level 403 邏輯較特殊,此處簡化示範
final String level;
final int sheetIndex, startRow, nameCol, hintCol;
ParseStrategy(String level, int sheetIndex, int startRow, int nameCol, int hintCol) {
this.level = level; this.sheetIndex = sheetIndex; this.startRow = startRow;
this.nameCol = nameCol; this.hintCol = hintCol;
}
static ParseStrategy fromLevel(String level) {
for (ParseStrategy s : values()) if (s.level.equals(level)) return s;
throw new IllegalArgumentException("Unsupported level: " + level);
}
}
/** 模擬原碼隨機時間生成 */
private String generateRandomTime() {
int sec = (int) (Math.random() * 10 + 1);
return String.format("00:%02d:00", sec);
}
}
| 主題 | 說明 |
|---|---|
| 介面導向 | 盡量使用 Workbook、Sheet、Row、Cell 介面,而非具體實作類,支援雙格式無縫切換。 |
| 資源管理 | 採用 try-with-resources 確保 InputStream 與 Workbook 正確關閉,防止記憶體洩漏。 |
| 儲存格型別 | Excel 儲存格可能為數值、日期、布林、公式,getStringCellValue() 會拋出異常,務必依 getCellType() 分派處理。 |
| 大檔案處理 | 若檔案超過百 MB,建議改用 SXSSFWorkbook (串流寫入) 或 XSSFReader (SAX 事件式讀取),避免 OOM。 |
| 策略模式 | 將不同 level 的 Sheet 索引、起始行、欄位映射封裝為枚舉,消除重複 if-else,易於擴充。 |
DateUtil.isCellDateFormatted(cell) 區分。FormulaEvaluator.evaluateAll(workbook) 或單獨 evaluateInCell(cell)。sheet.getMergedRegions() 取得區域,邏輯自行填補。list.clear()、考慮串流 API。掌握 Workbook 工廠、介面導向、型別安全讀取、策略封裝、資源釋放 五大核心,即可建立穩健、可維護的 Excel 解析模組。實務上建議再封裝為通用 ExcelReader<T> 泛型工具,搭配註解映射欄位,進一步提升開發效率。