SpringBoot实战:Excel文件上传、数据验证与存储全流程解析

发布于:2025-04-05 ⋅ 阅读:(9) ⋅ 点赞:(0)

一、需求场景与技术选型

在企业管理、数据中台等系统中,Excel文件处理是常见需求。本文将基于SpringBoot实现以下核心功能:

  1. 支持.xls/.xlsx文件上传
  2. 数据完整性验证(非空、格式等)
  3. 业务数据验证(关联数据库校验)
  4. 异常数据记录与反馈
  5. 数据批量入库

技术栈

  • 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: 8080
  servlet:
    context-path: /excel-demo

spring:
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb
    username: sa
    password:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 100MB

mybatis-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<>();

    @Override
    public 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);
    }

    @Override
    public 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解析失败"));
    }
}

五、测试验证

  1. 准备测试文件(包含正确和错误数据)
  2. 使用Postman发送POST请求
curl -X POST -F "file=@test.xlsx" http://localhost:8080/api/excel/upload
  1. 查看响应结果:
{
    "success": true,
    "total": 150,
    "successCount": 132,
    "errorCount": 18,
    "errors": [
        {"row": 5, "errors": "部门不存在"},
        {"row": 17, "errors": "工号格式不正确"}
    ]
}
  1. 检查数据库记录
SELECT * FROM employee WHERE hire_date > '2023-01-01';

六、生产级优化建议

  1. 安全增强

    • 文件病毒扫描
    • 文件头校验防止伪装扩展名
    • 上传频率限制
  2. 性能优化

    • 使用Redis缓存部门数据
    • 多线程分片处理
    • 数据库批量插入优化
  3. 可观测性

    • 添加处理进度查询接口
    • 集成Prometheus监控指标
    • 详细操作日志记录
  4. 用户体验

    • 生成错误报告Excel
    • 支持断点续传
    • 邮件通知处理结果

完整代码示例已托管至GitHub:springboot-excel-demo

通过本文的实现,我们构建了一个健壮的Excel处理流程,能够应对企业级应用中的复杂数据处理需求。实际项目中可根据具体业务场景扩展验证规则和优化处理逻辑。


网站公告

今日签到

点亮在社区的每一天
去签到