背景
😎昨天领导叫我做一个表格导入数据库的功能,发给我一个xlsx
表格,格式如下:

(不是真实数据,这只是一个示例结构)
如图,这是一个有3
行表头的excel
文件,其中部分部分表头是三层嵌套的,自然是想到用EasyExcel
处理,众所周知,EasyExcel
通过实体类注解@ExcelProperty
来解析表格,非常方便。
✅其中,有2
种映射方式:
- 通过表头名称映射
- 通过列的位置映射
实体类代码示例:
java
@Data
@ExcelIgnoreUnannotated // 忽略没有注解的字段
public class PlayerRecord {
// @ExcelProperty("姓名")
@ExcelProperty(index = 0)
private String name;
// @ExcelProperty("年龄")
@ExcelProperty(index = 1)
private Integer age;
// @ExcelProperty({"单机游戏", "卡普空", "游戏数量"})
@ExcelProperty(index = 2)
private BigDecimal singleCapcomNum;
// @ExcelProperty({"单机游戏", "卡普空", "游戏价值"})
@ExcelProperty(index = 3)
private BigDecimal singleCapcomValue;
// @ExcelProperty({"单机游戏", "2K GAMES", "游戏数量"})
@ExcelProperty(index = 4)
private BigDecimal singleTaketwoNum;
// @ExcelProperty({"单机游戏", "2K GAMES", "游戏价值"})
@ExcelProperty(index = 5)
private BigDecimal singleTaketwoValue;
// @ExcelProperty({"单机游戏", "战马工作室", "游戏数量"})
@ExcelProperty(index = 6)
private BigDecimal singleWarhorseNum;
// @ExcelProperty({"单机游戏", "战马工作室", "游戏价值"})
@ExcelProperty(index = 7)
private BigDecimal singleWarhorseValue;
// @ExcelProperty({"网络游戏", "腾讯", "游戏数量"})
@ExcelProperty(index = 8)
private BigDecimal webTencentNum;
// @ExcelProperty({"网络游戏", "腾讯", "氪金总额"})
@ExcelProperty(index = 9)
private BigDecimal webTencentValue;
// @ExcelProperty({"网络游戏", "网易", "游戏数量"})
@ExcelProperty(index = 10)
private BigDecimal webNetEaseNum;
// @ExcelProperty({"网络游戏", "网易", "氪金总额"})
@ExcelProperty(index = 11)
private BigDecimal webNetEaseValue;
// @ExcelProperty({"网络游戏", "米哈游", "游戏数量"})
@ExcelProperty(index = 12)
private BigDecimal webMihoyoNum;
// @ExcelProperty({"网络游戏", "米哈游", "氪金总额"})
@ExcelProperty(index = 13)
private BigDecimal webMihoyoValue;
// @ExcelProperty("职业")
@ExcelProperty(index = 14)
private String job;
// @ExcelProperty("创建时间")
@ExcelProperty(index = 15)
@DateTimeFormat("yyyy-MM-dd")
private LocalDate localDate;
// 测试用
private Integer year;
private Integer month;
private Integer day;
private Integer quarter;
}
⛔上面的代码中,被注释掉的为表头名映射。
问题
实际上对于上面的复杂嵌套表头,表头名映射很容易出现问题。
通过表头名映射多层表头的注解格式为:
java
@ExcelProperty({"单机游戏", "卡普空", "游戏数量"})
🤯然而,在列合并的情况下,实际上合并的多列中只有一列实际上有值,所以对于列合并的嵌套表头,经常会出现读取数据的丢失。
有列合并的嵌套表头,如:

♻建议直接使用headRowNumber
(表头行数)+index
来读取内容,实体类示例代码在上面已经给出。
解决
封装工具类,灵活适应不同的表头行数:
java
public class ExcelImportUtil {
/**
* 通过MultipartFile读取Excel文件,适用web端上传的Excel文件
*
* @param file 文件
* @param sheetRowNum 表头行数
* @return List 封装的实体类列表
*/
public static List<PlayerRecord> readExcel(MultipartFile file, int sheetRowNum) throws IOException {
List<PlayerRecord> list = new ArrayList<>();
EasyExcel.read(file.getInputStream(), PlayerRecord.class, new AnalysisEventListener<PlayerRecord>() {
@Override
public void invoke(PlayerRecord data, AnalysisContext context) {
if (data.getLocalDate() != null) {
// 这里可以添加任何额外操作逻辑
data.setDay(data.getLocalDate().getDayOfMonth());
data.setMonth(data.getLocalDate().getMonthValue());
data.setYear(data.getLocalDate().getYear());
data.setQuarter(data.getLocalDate().getMonthValue() / 3 + 1);
}
list.add(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}).sheet().headRowNumber(sheetRowNum).doRead();
return list;
}
/**
* File读取Excel文件,适用本地文件读取的Excel文件
*
* @param file 文件
* @param sheetRowNum 表头行数
* @return List 封装的实体类列表
*/
public static List<PlayerRecord> readExcel(File file, int sheetRowNum) throws IOException {
List<PlayerRecord> list = new ArrayList<>();
EasyExcel.read(file, PlayerRecord.class, new AnalysisEventListener<PlayerRecord>() {
@Override
public void invoke(PlayerRecord data, AnalysisContext context) {
if (data.getLocalDate() != null) {
// 这里可以添加任何额外操作逻辑
data.setDay(data.getLocalDate().getDayOfMonth());
data.setMonth(data.getLocalDate().getMonthValue());
data.setYear(data.getLocalDate().getYear());
data.setQuarter(data.getLocalDate().getMonthValue() / 3 + 1);
}
list.add(data);
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
}
}).sheet().headRowNumber(sheetRowNum).doRead();
return list;
}
}
📄上面的代码中,.headRowNumber
属性表示的是在读取的时候跳过初始的行数。
比如,在背景中提到的数据表结构,表头有3
行,此外开头还有2
行空行,所以设置sheetRowNum
为5
即可正确读取内容并封装为实体类。
✔如果表格上方没有空白的2
行:
| 姓名 | 年龄 | 单机游戏 |||||| 网络游戏 |||||| 职业 | 创建时间 |
| 姓名 | 年龄 | 卡普空 || 2K GAMES || 战马工作室 || 腾讯 || 网易 || 米哈游 || 职业 | 创建时间 |
姓名 | 年龄 | 游戏数量 | 游戏价值 | 游戏数量 | 游戏价值 | 游戏数量 | 游戏价值 | 游戏数量 | 氪金总额 | 游戏数量 | 游戏价值 | 游戏数量 | 游戏价值 | 职业 | 创建时间 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
张三 | 23 | 6 | 945.7 | 5 | 622.1 | 2 | 301.2 | 2 | 1033.4 | 3 | 3211.5 | 3 | 2344.5 | java程序员 | 2025-06-07 |
李四 | 33 | 7 | 1123.3 | 4 | 443.5 | 1 | 37.7 | 1 | 3333.2 | 2 | 1335.6 | 1 | 12 | 电器工程师 | 2025-03-05 |
设置headRowNumber
为3
即可。(上面的表格可以直接复制)
测试
编写测试类:
java
@Slf4j
public class EasyExcelTests {
private static final String TEST_FILE_PATH = "{你的测试文件绝对路径}";
@Test
public void test() throws IOException {
List<PlayerRecord> playerRecords = ExcelImportUtil
.readExcel(new File(TEST_FILE_PATH), 5);
log.info("===>playerRecords: {}", playerRecords);
}
}
运行,测试结果:
bash
已连接到地址为 ''127.0.0.1:51816',传输: '套接字'' 的目标虚拟机
11:03:17.403 [main] INFO com.losgai.ai.EasyExcelTests -- ===>playerRecords:
[PlayerRecord(
name=张三,
age=23,
singleCapcomNum=6,
singleCapcomValue=945.7,
singleTaketwoNum=5,
singleTaketwoValue=622.1,
singleWarhorseNum=2,
singleWarhorseValue=301.2,
webTencentNum=2,
webTencentValue=1033.4,
webNetEaseNum=3,
webNetEaseValue=3211.5,
webMihoyoNum=3,
webMihoyoValue=2344.5,
job=java程序员,
localDate=2025-06-07,
year=2025,
month=6,
day=7,
quarter=3),
PlayerRecord(
name=李四,
age=33,
singleCapcomNum=7,
singleCapcomValue=1123.3,
singleTaketwoNum=4,
singleTaketwoValue=443.5,
singleWarhorseNum=1,
singleWarhorseValue=37.7,
webTencentNum=1,
webTencentValue=3333.2,
webNetEaseNum=2,
webNetEaseValue=1335.6,
webMihoyoNum=1,
webMihoyoValue=12,
job=电器工程师,
localDate=2025-03-05,
year=2025,
month=3,
day=5,
quarter=2)]
已与地址为 ''127.0.0.1:51816',传输: '套接字'' 的目标虚拟机断开连接
进程已结束,退出代码为 0
🎉这样,我们就成功的读取到了表格数据,并封装为了实体类列表!
之后,封装一个接口,执行数据库批量插入操作即可。