一、需求場景與技術選型
在企業管理、數據中臺等系統中,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處理流程,能夠應對企業級應用中的復雜數據處理需求。實際項目中可根據具體業務場景擴展驗證規則和優化處理邏輯。