一、需求场景与技术选型
在企业管理、数据中台等系统中,Excel文件处理是常见需求。本文将基于SpringBoot实现以下核心功能:
- 支持.xls/.xlsx文件上传
- 数据完整性验证(非空、格式等)
- 业务数据验证(关联数据库校验)
- 异常数据记录与反馈
- 数据批量入库
技术栈:
- SpringBoot 2.7+
- Apache POI + EasyExcel
- MyBatis-Plus 3.5+
- H2 Database(演示用)
二、环境准备
2.1 添加依赖
<!-- Web支持 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><!-- Excel处理 -->
<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.1.1</version>
</dependency><!-- 数据库 -->
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.1</version>
</dependency><!-- 工具类 -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
2.2 配置文件
server:port: 8080servlet:context-path: /excel-demospring:datasource:driver-class-name: org.h2.Driverurl: jdbc:h2:mem:testdbusername: sapassword:servlet:multipart:max-file-size: 50MBmax-request-size: 100MBmybatis-plus:configuration:map-underscore-to-camel-case: true
三、核心实现
3.1 文件上传接口
@RestController
@RequestMapping("/api/excel")
@Slf4j
public class ExcelImportController {@PostMapping("/upload")public ResponseEntity<Map<String, Object>> uploadExcel(@RequestParam("file") MultipartFile file) {try {String originalFilename = file.getOriginalFilename();if (!originalFilename.matches("^.+\\.(?i)(xls|xlsx)$")) {return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Invalid file format"));}String savePath = "/tmp/uploads/";File dest = new File(savePath + originalFilename);if (!dest.getParentFile().exists()) {dest.getParentFile().mkdirs();}file.transferTo(dest);// 调用处理服务ImportResult result = excelService.processExcel(dest);return ResponseEntity.ok().body(result.toMap());} catch (Exception e) {log.error("File upload failed", e);return ResponseEntity.internalServerError().body(Collections.singletonMap("error", e.getMessage()));}}
}
3.2 数据模型与校验规则
@Data
public class EmployeeDTO {@ExcelProperty("员工姓名")@NotBlank(message = "姓名不能为空")private String name;@ExcelProperty("员工工号")@Pattern(regexp = "\\d{8}", message = "工号格式不正确")private String employeeId;@ExcelProperty("所属部门")private String department;@ExcelProperty("入职日期")@DateTimeFormat(pattern = "yyyy-MM-dd")private Date hireDate;
}
3.3 Excel解析与校验
public class EmployeeDataListener extends AnalysisEventListener<EmployeeDTO> {private static final int BATCH_SIZE = 100;private final List<EmployeeDTO> validData = new ArrayList<>();private final List<Map<String, String>> errorList = new ArrayList<>();@Overridepublic void invoke(EmployeeDTO data, AnalysisContext context) {// 基础校验Set<ConstraintViolation<EmployeeDTO>> violations = Validation.buildDefaultValidatorFactory().getValidator().validate(data);// 业务校验if (!departmentService.existByName(data.getDepartment())) {violations.add(new ConstraintViolationImpl<>("部门不存在", null, null, null, null, null));}if (!violations.isEmpty()) {handleErrors(context.readRowHolder().getRowIndex(), violations);return;}validData.add(data);if (validData.size() >= BATCH_SIZE) {saveBatch();validData.clear();}}private void handleErrors(Integer rowNum, Set<ConstraintViolation<?>> violations) {Map<String, String> error = new HashMap<>();error.put("row", String.valueOf(rowNum + 1));error.put("errors", violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")));errorList.add(error);}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {if (!validData.isEmpty()) {saveBatch();}}private void saveBatch() {employeeService.saveBatch(validData.stream().map(this::convertToEntity).collect(Collectors.toList()));}
}
3.4 服务层实现
@Service
@RequiredArgsConstructor
public class ExcelImportService {private final EmployeeService employeeService;private final DepartmentService departmentService;public ImportResult processExcel(File excelFile) {try (ExcelReader excelReader = EasyExcel.read(excelFile, EmployeeDTO.class, new EmployeeDataListener(employeeService, departmentService)).build()) {ReadSheet readSheet = EasyExcel.readSheet(0).headRowNumber(1).build();excelReader.read(readSheet);return ImportResult.success().errorRecords(listener.getErrorList()).totalCount(listener.getTotalCount()).build();}}
}
四、关键问题处理
4.1 大文件处理优化
- 使用SXSSF模式(POI的流式API)
- 分批次提交数据库事务
- 异步处理(@Async + 线程池)
@Async("excelTaskExecutor")
public Future<ImportResult> asyncProcess(File file) {// 处理逻辑
}
4.2 数据验证策略
验证类型 | 实现方式 | 示例 |
---|---|---|
格式验证 | JSR-303注解校验 | @Pattern(regexp=…) |
业务逻辑验证 | 数据库查询校验 | 部门是否存在 |
唯一性验证 | 数据库唯一索引+缓存去重 | 工号唯一性 |
关联数据验证 | 预加载缓存数据批量校验 | 预加载部门列表 |
4.3 异常处理机制
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(MultipartException.class)public ResponseEntity<?> handleSizeExceeded() {return ResponseEntity.status(HttpStatus.PAYLOAD_TOO_LARGE).body(Collections.singletonMap("error", "文件大小超过限制"));}@ExceptionHandler(ExcelAnalysisException.class)public ResponseEntity<?> handleExcelError() {return ResponseEntity.badRequest().body(Collections.singletonMap("error", "Excel解析失败"));}
}
五、测试验证
- 准备测试文件(包含正确和错误数据)
- 使用Postman发送POST请求
curl -X POST -F "file=@test.xlsx" http://localhost:8080/api/excel/upload
- 查看响应结果:
{"success": true,"total": 150,"successCount": 132,"errorCount": 18,"errors": [{"row": 5, "errors": "部门不存在"},{"row": 17, "errors": "工号格式不正确"}]
}
- 检查数据库记录
SELECT * FROM employee WHERE hire_date > '2023-01-01';
六、生产级优化建议
-
安全增强:
- 文件病毒扫描
- 文件头校验防止伪装扩展名
- 上传频率限制
-
性能优化:
- 使用Redis缓存部门数据
- 多线程分片处理
- 数据库批量插入优化
-
可观测性:
- 添加处理进度查询接口
- 集成Prometheus监控指标
- 详细操作日志记录
-
用户体验:
- 生成错误报告Excel
- 支持断点续传
- 邮件通知处理结果
完整代码示例已托管至GitHub:springboot-excel-demo
通过本文的实现,我们构建了一个健壮的Excel处理流程,能够应对企业级应用中的复杂数据处理需求。实际项目中可根据具体业务场景扩展验证规则和优化处理逻辑。