在 Spring Boot 中处理文件导入并校验第一列是否重复,推荐使用 阿里巴巴 EasyExcel (内存占用低、API 简洁)。下面提供两种实现方式:流式监听法(推荐,适合大文件) 和 全量内存法(适合中小文件),并附完整示例。
1. 引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.3</version>
</dependency>
2. 定义接收实体类
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
@Data
public class ImportRowDTO {
// index = 0 表示读取第一列(A列)
@ExcelProperty(index = 0)
private String firstColumnValue;
}
✅ 方案一:流式监听校验(推荐,内存友好)
逐行读取,边读边判断,不会将整个文件加载到内存,适合万行以上数据。
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.ReadListener;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
public class FirstColumnCheckListener implements ReadListener<ImportRowDTO> {
private final Set<String> seenValues = new HashSet<>();
private final List<String> duplicateRecords = new ArrayList<>();
private int currentRow = 0;
@Override
public void invoke(ImportRowDTO data, AnalysisContext context) {
currentRow = context.readRowHolder().getRowIndex() + 1; // Excel 行号从 1 开始(EasyExcel 默认跳过表头)
String value = data.getFirstColumnValue();
if (value == null) return; // 跳过空值(可根据业务调整)
String trimmed = value.trim();
// 如需忽略大小写:String trimmed = value.trim().toLowerCase();
if (!seenValues.add(trimmed)) {
duplicateRecords.add("第 " + currentRow + " 行: " + trimmed);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.info("第一列重复校验完成,共发现 {} 处重复", duplicateRecords.size());
}
public List<String> getDuplicateRecords() {
return duplicateRecords;
}
}
Service 层调用
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.excel.EasyExcel;
import java.io.IOException;
import java.util.List;
@Service
public class FileImportService {
public void validateFirstColumn(MultipartFile file) throws IOException {
FirstColumnCheckListener listener = new FirstColumnCheckListener();
EasyExcel.read(file.getInputStream(), ImportRowDTO.class, listener)
.sheet()
.doRead(); // 触发流式读取
List<String> duplicates = listener.getDuplicateRecords();
if (!duplicates.isEmpty()) {
// 抛出业务异常或返回错误列表
throw new IllegalArgumentException("第一列存在重复值:\n" + String.join("\n", duplicates));
}
}
}
✅ 方案二:全量读取后校验(代码更简短,适合小文件)
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.alibaba.excel.EasyExcel;
public void validateFirstColumnSimple(MultipartFile file) throws IOException {
List<ImportRowDTO> list = EasyExcel.read(file.getInputStream(), ImportRowDTO.class, null)
.sheet()
.doReadSync(); // 同步读取全部数据到内存
// 过滤空值 → 按值分组统计 → 筛选出现次数 >1 的项
List<String> duplicates = list.stream()
.map(ImportRowDTO::getFirstColumnValue)
.filter(Objects::nonNull)
.map(String::trim)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.filter(entry -> entry.getValue() > 1)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
if (!duplicates.isEmpty()) {
throw new IllegalArgumentException("第一列重复值: " + String.join(", ", duplicates));
}
}
📌 Controller 示例
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/import")
public class ImportController {
private final FileImportService fileImportService;
public ImportController(FileImportService fileImportService) {
this.fileImportService = fileImportService;
}
@PostMapping("/check-duplicate")
public String checkFirstColumn(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) return "文件不能为空";
try {
fileImportService.validateFirstColumn(file);
return "✅ 第一列无重复,校验通过";
} catch (Exception e) {
return "❌ " + e.getMessage();
}
}
}
💡 关键注意事项
| 场景 | 处理建议 |
|---|---|
| 空值是否算重复 | 当前代码跳过 null,如需将空字符串视为重复,改为 if (value == null) value = ""; |
| 忽略大小写 | String trimmed = value.trim().toLowerCase(); |
| 去除首尾空格 | 已内置 .trim(),防止 "ABC " 和 "ABC" 被误判为不同值 |
| CSV 文件支持 | EasyExcel 默认支持 .csv,无需修改代码 |
| 性能对比 | 流式监听:O(n) 时间,O(k) 内存(k 为不重复值数量)<br>全量读取:O(n) 时间,O(n) 内存 |
| 返回结构化错误 | 建议封装 List<RowError> 返回前端,便于高亮展示错误行 |
如需将校验结果与后续入库逻辑结合,可在 doAfterAllAnalysed 中触发事务方法,或使用 @Transactional 包裹导入流程。如有具体业务场景(如:重复时跳过/覆盖/报错),可进一步调整逻辑。
以下代码会读取excel表头吗 List<ImportRowDTO> list = EasyExcel.read(file.getInputStream(), ImportRowDTO.class, null) .sheet() .doReadSync();
默认情况下,不会读取表头。
📖 原理说明
EasyExcel 默认配置 headRowNumber = 1,即认为 Excel 的第一行是表头 。在执行 doReadSync() 时,它会自动跳过第 1 行,只从第 2 行开始解析数据并返回到 List 中。
🔧 如果你想包含表头一起读取
需要显式设置 .headRowNumber(0):
List<ImportRowDTO> list = EasyExcel.read(file.getInputStream(), ImportRowDTO.class, null)
.sheet()
.headRowNumber(0) // 0 表示不跳过任何行,第一行也会作为普通数据读取
.doReadSync();
⚠️ 重要注意事项
| 场景 | 影响 |
|---|---|
| 保持默认(跳过表头) | ✅ 推荐。表头不会被解析进 List,业务代码无需额外过滤,直接按数据行处理即可。 |
设置 headRowNumber(0) |
表头内容(如 "用户ID"、"姓名")会被当作普通字符串解析到 ImportRowDTO 的字段中。由于你的第一列是 String 类型,通常不会报错,但需在后续逻辑中手动剔除或标记。 |
| 字段映射方式 | 如果你的实体类使用 @ExcelProperty(index = 0),无论是否包含表头,都会按列索引映射;如果使用 @ExcelProperty("列名"),EasyExcel 会先匹配表头名称,再按规则解析数据行。 |
💡 针对你的"第一列重复校验"场景
建议保持默认(不读取表头) 。因为表头本身通常不参与业务数据校验,跳过表头能避免将 "第一列标题" 误判为重复值或引发类型转换问题。
如需进一步控制读取起始行或处理复杂表头(如多行表头),可参考 EasyExcel 官方文档的 headRowNumber 与 useDefaultListener 配置。