好的!让我用更简洁直观的方式说明这三个方法的执行时机。
执行时机总结
Excel 文件结构:
┌─────────────────────────────────┐
│ 第 1 行 │
│ ┌───┬───┬───┐ │
│ │A1 │B1 │C1 │ │
│ └───┴───┴───┘ │
├─────────────────────────────────┤
│ 第 2 行 │
│ ┌───┬───┬───┐ │
│ │A2 │B2 │C2 │ │
│ └───┴───┴───┘ │
└─────────────────────────────────┘
解析流程:
第 1 行:
↓
startRow(0) ← 遇到第 1 行开始
↓
cell("A1", "...") ← 遇到 A1 单元格
↓
cell("B1", "...") ← 遇到 B1 单元格
↓
cell("C1", "...") ← 遇到 C1 单元格
↓
endRow(0) ← 遇到第 1 行结束
↓
第 2 行:
↓
startRow(1) ← 遇到第 2 行开始
↓
cell("A2", "...") ← 遇到 A2 单元格
↓
cell("B2", "...") ← 遇到 B2 单元格
↓
cell("C2", "...") ← 遇到 C2 单元格
↓
endRow(1) ← 遇到第 2 行结束
↓
... 继续下一行
三个方法的触发条件
1. startRow(int rowNum)
触发时间:
- 当 SAX 解析器遇到
<row>标签时
触发条件:
- ✅ 每一行的开始都会触发
- ✅ 包括空行
- ✅ 表头行也会触发
示例:
java
// Excel 第 1 行开始
startRow(0) // ← 触发
// Excel 第 2 行开始
startRow(1) // ← 触发
// Excel 第 3 行(空行)开始
startRow(2) // ← 仍然触发
2. cell(String cellReference, String formattedValue, XSSFComment comment)
触发时间:
- 当 SAX 解析器遇到
<c>标签时
触发条件:
- ✅ 每个有数据的单元格都会触发
- ✅ 空单元格(有值标签但值为空)也会触发
- ❌ 完全没有值标签的单元格不会触发
示例:
java
// 第 1 行的单元格
cell("A1", "姓名", null) // ← 触发
cell("B1", "年龄", null) // ← 触发
cell("C1", "邮箱", null) // ← 触发
// 第 2 行的单元格
cell("A2", "张三", null) // ← 触发
cell("B2", "25", null) // ← 触发
cell("C2", "test@qq.com", null) // ← 触发
// 第 3 行(空行)
// 没有任何 cell 被触发
3. endRow(int rowNum)
触发时间:
- 当 SAX 解析器遇到
</row>标签时
触发条件:
- ✅ 每一行的结束都会触发
- ✅ 包括空行
- ✅ 表头行也会触发
示例:
java
// Excel 第 1 行结束
endRow(0) // ← 触发
// Excel 第 2 行结束
endRow(1) // ← 触发
// Excel 第 3 行(空行)结束
endRow(2) // ← 仍然触发
完整的执行顺序示例
示例 Excel
| A | B | C | |
|---|---|---|---|
| 1 | 姓名 | 年龄 | 邮箱 |
| 2 | 张三 | 25 | test@qq.com |
| 3 | |||
| 4 | 李四 | 30 | test2@qq.com |
执行顺序
时间轴:
t1: startRow(0) // 第 1 行开始
t2: cell("A1", "姓名", null)
t3: cell("B1", "年龄", null)
t4: cell("C1", "邮箱", null)
t5: endRow(0) // 第 1 行结束
t6: startRow(1) // 第 2 行开始
t7: cell("A2", "张三", null)
t8: cell("B2", "25", null)
t9: cell("C2", "test@qq.com", null)
t10: endRow(1) // 第 2 行结束
t11: startRow(2) // 第 3 行开始(空行)
t12: endRow(2) // 第 3 行结束
// 注意:没有 cell 调用,因为第 3 行是空行
t13: startRow(3) // 第 4 行开始
t14: cell("A4", "李四", null)
t15: cell("B4", "30", null)
t16: cell("C4", "test2@qq.com", null)
t17: endRow(3) // 第 4 行结束
图解
正常行(有数据)
Excel: ┌───┬───┬───┐
│A1 │B1 │C1 │
└───┴───┴───┘
解析:
┌─────────────────────────────────────┐
│ startRow(0) ← 遇到 <row> │
│ ↓ │
│ cell("A1", "...") ← 遇到 <c> │
│ ↓ │
│ cell("B1", "...") ← 遇到 <c> │
│ ↓ │
│ cell("C1", "...") ← 遇到 <c> │
│ ↓ │
│ endRow(0) ← 遇到 </row> │
└─────────────────────────────────────┘
空行(没有数据)
Excel: ┌───┬───┬───┐
│ │ │ │ ← 空行
└───┴───┴───┘
解析:
┌─────────────────────────────────────┐
│ startRow(2) ← 遇到 <row> │
│ ↓ │
│ (没有 cell 调用) │
│ ↓ │
│ endRow(2) ← 遇到 </row> │
└─────────────────────────────────────┘
关键点总结
| 方法 | 触发时机 | 触发频率 | 是否包括空行 |
|---|---|---|---|
| startRow | 行开始 | 每行一次 | ✅ 是 |
| cell | 单元格 | 每个单元格一次 | ❌ 否(空行没有) |
| endRow | 行结束 | 每行一次 | ✅ 是 |
实际代码示例
java
private static class DemoHandler implements XSSFSheetXMLHandler.SheetContentsHandler {
private int rowNum = 0;
private int cellCount = 0;
@Override
public void startRow(int rowNum) {
System.out.println("📍 第 " + (rowNum + 1) + " 行开始");
this.rowNum = rowNum;
}
@Override
public void cell(String cellReference, String formattedValue, XSSFComment comment) {
cellCount++;
System.out.println(" 📦 单元格 " + cellReference + ": " + formattedValue);
}
@Override
public void endRow(int rowNum) {
System.out.println("🏁 第 " + (rowNum + 1) + " 行结束(共 " + cellCount + " 个单元格)");
cellCount = 0; // 重置单元格计数
}
@Override
public void headerFooter(String text, boolean isHeader, String tagName) {
// 忽略
}
}
输出示例:
📍 第 1 行开始
📦 单元格 A1: 姓名
📦 单元格 B1: 年龄
📦 单元格 C1: 邮箱
🏁 第 1 行结束(共 3 个单元格)
📍 第 2 行开始
📦 单元格 A2: 张三
📦 单元格 B2: 25
📦 单元格 C2: test@qq.com
🏁 第 2 行结束(共 3 个单元格)
📍 第 3 行开始
🏁 第 3 行结束(共 0 个单元格)
📍 第 4 行开始
📦 单元格 A4: 李四
📦 单元格 B4: 30
📦 单元格 C4: test2@qq.com
🏁 第 4 行结束(共 3 个单元格)
一句话总结
- startRow:每行开始时调用一次
- cell:每个单元格调用一次(在 startRow 和 endRow 之间)
- endRow:每行结束时调用一次
执行顺序: startRow → cell (多次) → endRow