EasyExcel读取多层嵌套表头数据

背景

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

(不是真实数据,这只是一个示例结构)

如图,这是一个有3行表头的excel文件,其中部分部分表头是三层嵌套的,自然是想到用EasyExcel处理,众所周知,EasyExcel通过实体类注解@ExcelProperty来解析表格,非常方便。

✅其中,有2种映射方式:

  1. 通过表头名称映射
  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行空行,所以设置sheetRowNum5即可正确读取内容并封装为实体类。

✔如果表格上方没有空白的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

设置headRowNumber3即可。(上面的表格可以直接复制)

测试

编写测试类:

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

🎉这样,我们就成功的读取到了表格数据,并封装为了实体类列表!

之后,封装一个接口,执行数据库批量插入操作即可。

相关推荐
JH30731 小时前
Java Stream API 在企业开发中的实战心得:高效、优雅的数据处理
java·开发语言·oracle
九月十九4 小时前
java使用aspose读取word里的图片
java·word
一 乐5 小时前
民宿|基于java的民宿推荐系统(源码+数据库+文档)
java·前端·数据库·vue.js·论文·源码
爱记录的小磊5 小时前
java-selenium自动化快速入门
java·selenium·自动化
鹏码纵横5 小时前
已解决:java.lang.ClassNotFoundException: com.mysql.jdbc.Driver 异常的正确解决方法,亲测有效!!!
java·python·mysql
weixin_985432115 小时前
Spring Boot 中的 @ConditionalOnBean 注解详解
java·spring boot·后端
Mr Aokey5 小时前
Java UDP套接字编程:高效实时通信的实战应用与核心类解析
java·java-ee
冬天vs不冷5 小时前
Java分层开发必知:PO、BO、DTO、VO、POJO概念详解
java·开发语言
hong_zc5 小时前
Java 文件操作与IO流
java·文件操作·io 流