后端 Excel 导入实战:从文件解析到行级校验

文件导入功能实现

在很多后台系统里,Excel 导入 几乎是必不可少的功能。

但真正好用、稳定、可维护的导入功能并不多见:

  • 有的只能粗暴导入,不校验数据
  • 有的校验不完善,报错不明确
  • 有的中途失败会造成部分数据入库
  • 有的解析慢,一个几 MB 的 Excel 能卡几十秒
  • 有的用户看到报错完全不知道错在哪里

最近我在做一个数据导入功能时,顺手整理了我设定的一套 后端 Excel 导入流程体系

包含:

  • 文件解析 ✓
  • Excel 表头校验 ✓
  • 行级字段校验 ✓
  • 组合唯一性校验 ✓
  • 数据对象转换 ✓
  • 业务规则计算 ✓
  • 批量落库 ✓
  • 异步处理额外逻辑 ✓

这篇文章就把核心的技术设计 + 踩坑点分享出来,希望对你有帮助。


一、 📥 文件解析:为什么选择 EasyExcel?

Java 解析 Excel 的方案很多,POI、JXL 都可以。但在大文件场景下:

  • POI 容易 OOM
  • JXL 老旧不维护
  • EasyExcel 解析速度快 & 流式处理不占内存

所以我封装了一个通用方法如下,能实现表头自动与实体类注解比对、数据逐行读取,不占内存、自动封装成 Java 实体类、表格为空时自动报错。这是整个导入流程最核心的逻辑。

java 复制代码
public static <T> List<T> readMultipartFile(InputStream inputStream, Class<T> clazz) {
    final List<T> dataList = new ArrayList<>();
    EasyExcel.read(inputStream, clazz, new AnalysisEventListener<T>() {

        // 表头校验
        public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
            List<String> expectedHeaders = readExcelPropertyAnnotations(clazz);
            List<String> actualHeaders = headMap.values().stream()
                    .filter(h -> h != null && !h.trim().isEmpty())
                    .toList();
            if (!actualHeaders.equals(expectedHeaders)) {
                throw new BusinessException("Excel 表头不匹配");
            }
        }

        // 每行数据回调
        @Override
        public void invoke(T data, AnalysisContext context) {
            dataList.add(data);
        }

        // 读取完成后可以进行一些操作
        @Override
	    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
		}

    }).sheet().doRead();

    if (dataList.isEmpty()) {
        throw new BusinessException("Excel 无数据");
    }
    return dataList;
}

二、🔍 Excel 行级校验:拒绝脏数据入库

我们这个业务场景主要处理的是自用户上传的Excel文件数据,并将其入库。但导入 Excel 最大的问题就是:**用户给的数据永远不干净,**入库的时候不可避免地需要对用户数据进行校验。

这里主要针对用户上传的日期格式进行校验,假设我们需要的是 **"yyyy-MM-dd"**的时间格式,不需要精确到时分秒,在数据入库之前,我们需要将校验逻辑统一放到循环中进行处理:

java 复制代码
for (int i = 0; i < excels.size(); i++) {
    WaterExcelRow row = excels.get(i);

    int realRowNum = i + 2; // 因为 Excel 从第2行开始是数据

    // ① 日期不能为空
    if (StringUtils.isBlank(row.getDate())) {
        return fail("第 " + realRowNum + " 行:日期不能为空");
    }

    // ② 日期格式校验 yyyy-MM-dd
    try {
        LocalDate.parse(row.getDate(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    } catch (Exception e) {
        return fail("第 " + realRowNum + " 行:日期格式错误(当前值:" + row.getDate() + ")");
    }

    // ③ 针对其他字段进行的校验...
    }
}

这个时候你可能有其他的问题:如何在导入数据时校验某个字段的唯一性或组合字段的唯一性?

**需求分析:**为什么需要某些字段的内部唯一性校验呢?因为用户总是喜欢复制粘贴的。或许多粘了一行或输入的两行数据一样自己却没有察觉,就将这样带着脏数据的 Excel 丢给了我们的接口。

场景示例: 你正在导入一份"学生某次考试的成绩单",系统要求 每个学生在同一次考试中只能出现一条成绩记录 。那么我们就需要在导入过程中,对**"学生 ID + 考试批次"**进行唯一性校验。

**做法:**在遍历 Excel 数据行时,将"学生ID+考试批次的组合 Key " 依次放入一个集合 Set 中。当某一行的数据尝试加入 Set 时,如果发现该元素已经存在,就说明出现了重复数据,当前 Excel 的这一组数据将被判定为不合规,此次导入也应该直接中止。

java 复制代码
// 用于校验唯一性的 Set(存 studentId + examBatch 的组合)
Set<String> uniqueKeySet = new HashSet<>();

for (int i = 0; i < excelList.size(); i++) {

    StudentScoreExcel row = excelList.get(i);

    // 1. 基本字段校验
    if (StringUtils.isBlank(row.getStudentId())) {
        return CommonResult.fail("第" + (i + 2) + "行:学生ID不能为空");
    }
    if (StringUtils.isBlank(row.getExamBatch())) {
        return CommonResult.fail("第" + (i + 2) + "行:考试批次不能为空");
    }

    // 2. 组合唯一性校验:studentId + examBatch
    String key = row.getStudentId() + "|" + row.getExamBatch();

    if (!uniqueKeySet.add(key)) {
        // add() 返回 false 表示已经存在该元素,即数据重复
        return CommonResult.fail(
                "第" + (i + 2) + "行:学生ID [" + row.getStudentId() + "] 在考试批次 [" +
                row.getExamBatch() + "] 中已有成绩记录,数据重复"
        );
    }

    // 3. 其他字段校验...
    // 比如成绩范围、日期格式、科目合法性等
}

三、🧩 数据校验通过后,再统一转换成实体对象

校验通过的行,会被转换成实际要入库的实体对象,这个就贴合你的需求实际,看你的入库实体包含什么字段、你会往里面添加什么字段入库了。一定要避免直接使用 Excel 行对象入库,因为:

  • Excel DTO 与数据库 Entity 结构不一致
  • Excel 格式不等于业务格式
  • Excel 注解多,会污染数据库实体类

四、💾 全量校验通过后再入库(避免部分入库)

这是非常关键的点。很多系统的导入做得很糟糕------

  • 遭遇第一条错误后,前几条数据已经入库
  • 一半成功、一半失败
  • 回滚不彻底

可采取的一个建议措施是:只要有错误,一行都不能入库。

在设计导入流程时,千万不要只给用户一句"导入失败"这种模糊提示。更好的做法是:在校验阶段就将所有问题收集起来,并清晰告知用户例如"第 X 行某字段格式错误""第 Y 行数据重复"等详细信息,让用户能够有针对性地修改文件后重新上传。这样既避免了用户反复尝试,又能防止出现"前半部分已经入库,后面某行失败导致整体回滚"的情况,使整个导入体验更加友好和可控。

五、🔄 异步处理:避免阻塞导入流程

某些逻辑(比如针对入库数据进行安全校验、对超标、不合规数据发送告警/调用其他系统)不能阻塞导入,可以考虑用线程池异步执行:

java 复制代码
Executors.newSingleThreadExecutor()
    .execute(() -> alarmClient.saveAlarm(alarmList));

这样:

  • 用户体验更好
  • 导入速度不受外部接口影响
  • 不会因为某系统卡顿而导致导入失败

六、📑 完整日志记录:无日志等于无真相

最后,把上传的文件存到对象存储或文件服务器,

并记录数据库日志:

java 复制代码
fileUpload(file);
insertLog(filename, fileId);

这样当用户说:"我导入成功了但你们系统没数据啊?"

你能用日志稳稳锤他。

七、🏁总结:一套成熟的 Excel 导入处理体系应该具备的能力

✔ 表头严格匹配

✔ Excel DTO → Entity 结构分离

✔ 行级校验

✔ 格式校验

✔ 唯一性校验

✔ 批量转换

✔ 业务规则计算

✔ 批量入库

✔ 失败即终止

✔ 异步扩展逻辑

✔ 文件日志记录

不是简单的"把 Excel 转成 List 吐给数据库"。

这是成熟系统必须做的事,也是一个优秀后端开发应该具备的思维。