parent-spring-tools

spring-tools-simples

spring-excel-mapper 的演示和示例应用,包含两种推荐的集成模式。

概述

本模块演示如何在实际的 Spring Boot 应用中使用 spring-excel-mapper。包括:

详细的 API 参考和设计文档,请参见 spring-excel-mapper README


使用模式

模式 1:直接 API 使用(完全控制)

当您需要对每个步骤进行最大控制或在导入过程中有复杂业务逻辑时,直接使用 ExcelMapper API。

步骤 1:定义 Excel 模型和 DTO

@Data
@ExcelSheet
@ExcelRow(startRowIndex = 1)
public class StudentExcelModel {
    @ExcelColumn(col = "A", required = true)
    private String name;
    
    @ExcelColumn(col = "B", regex = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")
    private String email;
    
    @ExcelColumn(col = "C", min = 18, max = 100)
    private Integer age;
}

@Data
public class StudentDTO {
    private String name;
    private String email;
    private Integer age;
}

步骤 2:创建 MapStruct Mapper

@Mapper(componentModel = "spring")
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
    
    StudentDTO toDTO(StudentExcelModel model);
    List<StudentDTO> toDTOs(List<StudentExcelModel> models);
}

步骤 3:使用直接 API 实现服务

@Service
@Slf4j
public class StudentExcelServiceManual {
    
    public ImportResult<StudentDTO> importStudents(MultipartFile file) throws Exception {
        try (InputStream is = file.getInputStream();
             Workbook workbook = WorkbookFactory.create(is)) {
            
            // 解析 Excel 模型
            ImportResult<StudentExcelModel> parseResult = 
                ExcelMapper.parseWorkbookWithResult(workbook, StudentExcelModel.class, false);
            
            if (!parseResult.isSuccess()) {
                // 处理错误
                ImportResult<StudentDTO> result = new ImportResult<>();
                result.setErrors(parseResult.getErrors());
                return result;
            }
            
            // 使用 MapStruct 转换为 DTO
            List<StudentDTO> dtoList = StudentMapper.INSTANCE.toDTOs(parseResult.getSuccessList());
            
            // 可选:在此处添加业务逻辑(例如,数据库保存)
            
            // 返回结果
            ImportResult<StudentDTO> result = new ImportResult<>();
            result.setSuccessList(dtoList);
            result.setTotalRows(dtoList.size());
            return result;
        }
    }
}

步骤 4:在控制器中使用

@RestController
@RequestMapping("/api/students")
public class StudentController {
    
    @Autowired
    private StudentExcelServiceManual excelService;
    
    @PostMapping("/import")
    public ResponseEntity<?> importStudents(@RequestParam("file") MultipartFile file) throws Exception {
        ImportResult<StudentDTO> result = excelService.importStudents(file);
        
        if (result.isSuccess()) {
            return ResponseEntity.ok(Map.of(
                "success", true,
                "data", result.getSuccessList(),
                "count", result.getTotalRows()
            ));
        } else {
            return ResponseEntity.badRequest().body(Map.of(
                "success", false,
                "errors", result.getErrors()
            ));
        }
    }
}

优点:

缺点:


模式 2:模板方法(推荐)

使用 ExcelImportService<ExcelModel, DTO> 抽象基类来获得更简洁的代码和统一的工作流。推荐用于大多数用例。

步骤 1:定义 Excel 模型和 DTO(与模式 1 相同)

@Data
@ExcelSheet
@ExcelRow(startRowIndex = 1)
public class StudentExcelModel {
    @ExcelColumn(col = "A", required = true)
    private String name;
    
    @ExcelColumn(col = "B", regex = "^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")
    private String email;
    
    @ExcelColumn(col = "C", min = 18, max = 100)
    private Integer age;
}

@Data
public class StudentDTO {
    private String name;
    private String email;
    private Integer age;
}

步骤 2:创建 MapStruct Mapper(与模式 1 相同)

@Mapper(componentModel = "spring")
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
    
    StudentDTO toDTO(StudentExcelModel model);
    List<StudentDTO> toDTOs(List<StudentExcelModel> models);
}

步骤 3:扩展 ExcelImportService(最小实现)

@Service
@Slf4j
public class StudentExcelImportService extends ExcelImportService<StudentExcelModel, StudentDTO> {
    
    @Override
    protected Class<StudentExcelModel> getExcelModelClass() {
        return StudentExcelModel.class;
    }
    
    @Override
    protected StudentDTO convertToDTO(StudentExcelModel model) {
        return StudentMapper.INSTANCE.toDTO(model);
    }
    
    @Override
    protected List<StudentDTO> convertToDTOList(List<StudentExcelModel> models) {
        return StudentMapper.INSTANCE.toDTOs(models);
    }
    
    // 可选的生命周期钩子:
    
    @Override
    protected void beforeImport(Workbook workbook) {
        log.info("开始导入学生,sheet 数量:{}", workbook.getNumberOfSheets());
    }
    
    @Override
    protected void afterImport(List<StudentDTO> dtoList) {
        log.info("导入完成,导入了 {} 条记录", dtoList.size());
        // 示例:保存到数据库、发送通知等
    }
    
    @Override
    protected void onImportError(Exception e) {
        log.error("导入失败", e);
        // 示例:发送警报、回滚事务等
    }
}

步骤 4:在控制器中使用(简化版)

@RestController
@RequestMapping("/api/students")
public class StudentController {
    
    @Autowired
    private StudentExcelImportService importService;
    
    @PostMapping("/import")
    public ResponseEntity<?> importStudents(@RequestParam("file") MultipartFile file) throws Exception {
        ImportResult<StudentDTO> result = importService.importExcel(file.getInputStream(), false);
        
        if (result.isSuccess()) {
            return ResponseEntity.ok(Map.of(
                "success", true,
                "data", result.getSuccessList(),
                "count", result.getTotalRows()
            ));
        } else {
            return ResponseEntity.badRequest().body(Map.of(
                "success", false,
                "errors", result.getErrors()
            ));
        }
    }
}

优点:

缺点:


常用代码片段

解析 Workbook 为列表

Workbook wb = WorkbookFactory.create(inputStream);
List<MyModel> list = ExcelMapper.parseWorkbook(wb, MyModel.class);

解析并收集错误

ImportResult<MyModel> result = 
    ExcelMapper.parseWorkbookWithResult(workbook, MyModel.class, false);
    
if (result.isSuccess()) {
    List<MyModel> data = result.getSuccessList();
} else {
    List<ValidationResult> errors = result.getErrors();
}

按 Sheet 名称解析

Map<String, List<MyModel>> perSheet = 
    ExcelMapper.parseWorkbookBySheet(wb, MyModel.class, true);

导出数据到模板

Sheet sheet = workbook.getSheet("Items");
ExcelExporter.fillTableRows(sheet, startRow, dataList, MyModel.class);

基于区段的解析

@ExcelSheet
@ExcelSection(
    sectionKeyword = "学生列表",
    keywordInCol = "A",
    startRowIndex = 1,
    stopKeyword = "结束"
)
public class StudentSection {
    @ExcelColumn(col = "A")
    private String name;
}

List<StudentSection> sections = ExcelMapper.parseSheetSection(sheet, StudentSection.class);

项目结构

spring-tools-simples/
├── src/main/java/org/springtools/excel/
│   ├── controller/          # REST 控制器
│   ├── service/             # 示例服务
│   │   ├── ExcelImportService.java              # 基础模板类
│   │   ├── ExcelServiceLegacy.java              # 模式 1 示例
│   │   └── impl/
│   │       ├── ClassImportService.java          # 模式 2 示例
│   │       ├── DepartmentImportService.java     # 模式 2 示例
│   │       ├── StudentImportService.java        # 模式 2 示例
│   │       └── ExcelMapperServiceImpl.java      # 组合导入/导出
│   ├── assembler/           # MapStruct mappers
│   ├── dto/                 # DTOs
│   └── excelmodel/          # Excel 模型类
└── src/main/resources/
    └── templates/           # 示例 Excel 模板

运行示例

构建与运行

# 构建项目
mvn clean package

# 运行 Spring Boot 应用
mvn -pl spring-tools-simples spring-boot:run

# 或运行 JAR
java -jar spring-tools-simples/target/spring-tools-simples-1.0.0-SNAPSHOT.jar

测试端点

启动应用后,您可以测试端点:

# 导入学生(模式 2)
curl -X POST http://localhost:8080/api/students/import \
  -F "file=@students.xlsx"

# 导入部门
curl -X POST http://localhost:8080/api/departments/import \
  -F "file=@departments.xlsx"

# 导出示例
curl -X GET http://localhost:8080/api/export/template \
  -o output.xlsx

高级示例

多区段导入

从单个 Excel 文件导入多个区段:

@Service
public class ExcelMapperServiceImpl {
    
    public CombinedResult importAll(MultipartFile file) throws Exception {
        try (InputStream is = file.getInputStream();
             Workbook workbook = WorkbookFactory.create(is)) {
            
            Sheet sheet = workbook.getSheetAt(0);
            
            // 解析不同区段
            List<DepartmentExcelModel> departments = 
                ExcelMapper.parseSheetSection(sheet, DepartmentExcelModel.class);
            
            List<ClassExcelModel> classes = 
                ExcelMapper.parseSheetSection(sheet, ClassExcelModel.class);
            
            List<StudentExcelModel> students = 
                ExcelMapper.parseSheetSection(sheet, StudentExcelModel.class);
            
            // 转换并组合
            CombinedResult result = new CombinedResult();
            result.setDepartments(convertDepartments(departments));
            result.setClasses(convertClasses(classes));
            result.setStudents(convertStudents(students));
            
            return result;
        }
    }
}

基于模板的导出

使用 Excel 模板导出数据(保留格式):

@Service
public class ExportService {
    
    public Workbook exportToTemplate(List<StudentDTO> students) throws Exception {
        // 加载模板
        try (InputStream template = getClass().getResourceAsStream("/templates/student_template.xlsx")) {
            Workbook workbook = WorkbookFactory.create(template);
            Sheet sheet = workbook.getSheet("Students");
            
            // 将 DTO 转换为 Excel 模型
            List<StudentExcelModel> models = StudentMapper.INSTANCE.toExcelModels(students);
            
            // 填充模板(从第 1 行开始,保留第 0 行的表头)
            ExcelExporter.fillTableRows(sheet, 1, models, StudentExcelModel.class);
            
            return workbook;
        }
    }
}

相关文档


提示与最佳实践

  1. 选择模式 2,除非您有需要模式 1 灵活性的复杂业务逻辑
  2. 使用 MapStruct 进行模型转换 — 它是类型安全的并生成高效的代码
  3. 优雅地处理错误 — 使用 parseWorkbookWithResult(..., failFast=false) 收集所有错误
  4. afterImport() 钩子中验证业务规则,而不是在解析期间
  5. 使用模板进行导出 以保留 Excel 格式和公式
  6. 测试边缘情况 — 空行、合并单元格、公式、大型数据集
  7. 在生命周期钩子中添加日志 用于调试和监控
  8. 使用表达式验证@ExcelColumn 支持 regexminmax 等多种验证方式