Handler 注册机制业务与导入导出解耦

作者:忆笙智云官方 | 发布时间:2026-06-13 09:30 | 更新时间:2026-07-13 09:30

Handler注册机制业务与导入导出解耦

引言

在企业级低代码平台中,导入导出是高频使用的功能。不同业务模块(用户、角色、部门、字典等)都需要支持 Excel 导入导出,但每个模块的数据结构、校验规则、业务逻辑各不相同。如果将所有导入导出逻辑写在一个"大而全"的服务类中,会导致严重的代码耦合和可维护性问题。

本文分享一种基于 Handler 注册机制的导入导出解耦方案:定义 ImportHandler / ExportHandler 接口,各业务模块注册自己的 Handler 实现,导入导出中心通过 Handler 调用业务逻辑,实现业务模块与导入导出框架的完全解耦。结合 Spring 依赖注入,Handler 的注册和发现完全自动化。

核心内容

一、问题分析

flowchart TB
    A[传统方案的问题] --> B[代码耦合严重]
    A --> C[职责不清晰]
    A --> D[扩展性差]
    A --> E[测试困难]

    B --> B1["导入导出服务中充斥着
if-else 判断业务类型"]
    C --> C1["一个类既管导入逻辑
又管导出逻辑
还管文件解析"]
    D --> D1["新增业务模块需要
修改导入导出服务代码"]
    E --> E1["无法单独测试
某个模块的导入逻辑"]

    style A fill:#ff6b6b,color:#fff

传统方案的代码通常长这样:

// ❌ 不推荐:大而全的导入导出服务
@Service
public class ImportExportService {

    public void doImport(String bizType, MultipartFile file) {
        if ("user".equals(bizType)) {
            // 用户导入逻辑:解析、校验、去重、保存...
        } else if ("role".equals(bizType)) {
            // 角色导入逻辑:解析、校验、权限处理、保存...
        } else if ("dept".equals(bizType)) {
            // 部门导入逻辑:解析、校验、树形结构处理、保存...
        }
        // 每新增一个业务类型就要加一个 else if
    }
}

二、Handler 注册机制设计

flowchart TB
    subgraph 导入导出中心
        A[ImportExportCenter]
        A --> B[HandlerRegistry
注册中心]
        B --> C[Handler Map
key: bizType
value: Handler]
    end

    subgraph 业务模块
        D[UserImportHandler]
        E[UserExportHandler]
        F[RoleImportHandler]
        G[RoleExportHandler]
        H[DeptImportHandler]
        I[DeptExportHandler]
    end

    D -->|注册| B
    E -->|注册| B
    F -->|注册| B
    G -->|注册| B
    H -->|注册| B
    I -->|注册| B

    J[导入请求] --> A
    A --> B
    B -->|查找Handler| D
    B -->|查找Handler| F
    B -->|查找Handler| H

    K[导出请求] --> A
    A --> B
    B -->|查找Handler| E
    B -->|查找Handler| G
    B -->|查找Handler| I

    style A fill:#4CAF50,color:#fff
    style B fill:#2196F3,color:#fff

三、Handler 接口设计

3.1 ImportHandler 接口

/**
 * 导入处理器接口
 * 各业务模块实现此接口以支持数据导入
 * @param <T> 导入数据行的DTO类型
 */
public interface ImportHandler<T> {

    /**
     * 获取业务类型标识
     * 用于注册到Handler注册中心,必须唯一
     * @return 业务类型,如 "user", "role"
     */
    String getBizType();

    /**
     * 获取导入数据的DTO类
     * 用于Excel解析时确定目标类型
     * @return DTO类对象
     */
    Class<T> getDtoClass();

    /**
     * 获取导入模板的列定义
     * 用于生成导入模板和校验表头
     * @return 列定义列表
     */
    List<ImportColumn> getColumns();

    /**
     * 数据校验
     * 在数据解析后、持久化前调用
     * @param dataList 解析后的数据列表
     * @return 校验结果,包含错误信息
     */
    ImportValidateResult validate(List<T> dataList);

    /**
     * 数据持久化
     * 校验通过后调用,将数据保存到数据库
     * @param dataList 校验通过的数据列表
     * @return 导入结果
     */
    ImportResult persist(List<T> dataList);

    /**
     * 导入前处理(可选)
     * 如:清除临时数据、初始化缓存等
     */
    default void beforeImport() {}

    /**
     * 导入后处理(可选)
     * 如:刷新缓存、发送通知等
     */
    default void afterImport(ImportResult result) {}
}

3.2 ExportHandler 接口

/**
 * 导出处理器接口
 * 各业务模块实现此接口以支持数据导出
 * @param <T> 导出数据行的DTO类型
 */
public interface ExportHandler<T> {

    /**
     * 获取业务类型标识
     * 与 ImportHandler 的 bizType 对应
     * @return 业务类型
     */
    String getBizType();

    /**
     * 获取导出数据的DTO类
     * @return DTO类对象
     */
    Class<T> getDtoClass();

    /**
     * 获取导出列定义
     * @return 列定义列表
     */
    List<ExportColumn> getColumns();

    /**
     * 查询导出数据
     * @param queryParam 查询参数
     * @return 导出数据列表
     */
    List<T> queryData(ExportQueryParam queryParam);

    /**
     * 导出文件名
     * @return 文件名(不含扩展名)
     */
    default String getFileName() {
        return getBizType() + "_" + System.currentTimeMillis();
    }
}

3.3 辅助类定义

/**
 * 导入列定义
 */
@Data
@Builder
public class ImportColumn {
    /** 列名(表头) */
    private String name;
    /** 对应字段名 */
    private String fieldName;
    /** 是否必填 */
    private boolean required;
    /** 字段类型(用于类型转换) */
    private Class<?> fieldType;
    /** 字典编码(用于字典值转换) */
    private String dictCode;
    /** 最大长度 */
    private int maxLength;
    /** 校验正则 */
    private String pattern;
}

/**
 * 导入校验结果
 */
@Data
@Builder
public class ImportValidateResult {
    /** 是否通过 */
    private boolean passed;
    /** 错误信息列表 */
    private List<ValidateError> errors;
}

/**
 * 校验错误
 */
@Data
@Builder
public class ValidateError {
    /** 行号 */
    private int rowIndex;
    /** 字段名 */
    private String fieldName;
    /** 错误信息 */
    private String message;
}

/**
 * 导入结果
 */
@Data
@Builder
public class ImportResult {
    /** 总行数 */
    private int totalCount;
    /** 成功行数 */
    private int successCount;
    /** 失败行数 */
    private int failCount;
    /** 失败详情 */
    private List<ValidateError> failDetails;
}

四、Handler 注册中心

4.1 注册中心实现

/**
 * Handler 注册中心
 * 通过 Spring 依赖注入自动发现和注册所有 Handler
 */
@Component
public class HandlerRegistry implements ApplicationContextAware {

    /** 导入Handler注册表 key: bizType, value: ImportHandler */
    private final Map<String, ImportHandler<?>> importHandlerMap = new ConcurrentHashMap<>();

    /** 导出Handler注册表 key: bizType, value: ExportHandler */
    private final Map<String, ExportHandler<?>> exportHandlerMap = new ConcurrentHashMap<>();

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        // 自动发现所有 ImportHandler 实现
        Map<String, ImportHandler> importHandlers =
            applicationContext.getBeansOfType(ImportHandler.class);
        importHandlers.values().forEach(handler -> {
            String bizType = handler.getBizType();
            importHandlerMap.put(bizType, handler);
            log.info("注册导入Handler: {} -> {}", bizType, handler.getClass().getSimpleName());
        });

        // 自动发现所有 ExportHandler 实现
        Map<String, ExportHandler> exportHandlers =
            applicationContext.getBeansOfType(ExportHandler.class);
        exportHandlers.values().forEach(handler -> {
            String bizType = handler.getBizType();
            exportHandlerMap.put(bizType, handler);
            log.info("注册导出Handler: {} -> {}", bizType, handler.getClass().getSimpleName());
        });
    }

    /**
     * 获取导入Handler
     * @param bizType 业务类型
     * @return ImportHandler
     * @throws BusinessException 如果Handler不存在
     */
    @SuppressWarnings("unchecked")
    public <T> ImportHandler<T> getImportHandler(String bizType) {
        ImportHandler<?> handler = importHandlerMap.get(bizType);
        if (handler == null) {
            throw new BusinessException("未找到业务类型 [%s] 的导入Handler", bizType);
        }
        return (ImportHandler<T>) handler;
    }

    /**
     * 获取导出Handler
     * @param bizType 业务类型
     * @return ExportHandler
     * @throws BusinessException 如果Handler不存在
     */
    @SuppressWarnings("unchecked")
    public <T> ExportHandler<T> getExportHandler(String bizType) {
        ExportHandler<?> handler = exportHandlerMap.get(bizType);
        if (handler == null) {
            throw new BusinessException("未找到业务类型 [%s] 的导出Handler", bizType);
        }
        return (ExportHandler<T>) handler;
    }

    /**
     * 获取所有已注册的业务类型
     */
    public List<BizTypeInfo> getRegisteredBizTypes() {
        Set<String> allBizTypes = new HashSet<>();
        allBizTypes.addAll(importHandlerMap.keySet());
        allBizTypes.addAll(exportHandlerMap.keySet());

        return allBizTypes.stream().map(bizType -> {
            BizTypeInfo info = new BizTypeInfo();
            info.setBizType(bizType);
            info.setSupportImport(importHandlerMap.containsKey(bizType));
            info.setSupportExport(exportHandlerMap.containsKey(bizType));
            return info;
        }).collect(Collectors.toList());
    }
}

五、导入导出中心

5.1 导入导出中心实现

/**
 * 导入导出中心
 * 统一的导入导出入口,通过 Handler 分发到各业务模块
 */
@Service
public class ImportExportCenter {

    @Autowired
    private HandlerRegistry handlerRegistry;

    @Autowired
    private ExcelParser excelParser;

    @Autowired
    private ExcelWriter excelWriter;

    /**
     * 执行导入
     * @param bizType 业务类型
     * @param file    上传的Excel文件
     * @return 导入结果
     */
    public <T> ImportResult doImport(String bizType, MultipartFile file) {
        // 1. 获取Handler
        ImportHandler<T> handler = handlerRegistry.getImportHandler(bizType);

        // 2. 导入前处理
        handler.beforeImport();

        // 3. 解析Excel文件
        List<T> dataList = excelParser.parse(file, handler.getDtoClass(), handler.getColumns());

        // 4. 数据校验
        ImportValidateResult validateResult = handler.validate(dataList);
        if (!validateResult.isPassed()) {
            return ImportResult.builder()
                .totalCount(dataList.size())
                .successCount(0)
                .failCount(dataList.size())
                .failDetails(validateResult.getErrors())
                .build();
        }

        // 5. 过滤校验失败的数据
        List<T> validData = filterValidData(dataList, validateResult);

        // 6. 数据持久化
        ImportResult result = handler.persist(validData);

        // 7. 导入后处理
        handler.afterImport(result);

        return result;
    }

    /**
     * 执行导出
     * @param bizType    业务类型
     * @param queryParam 查询参数
     * @return 导出文件字节数组
     */
    public <T> byte[] doExport(String bizType, ExportQueryParam queryParam) {
        // 1. 获取Handler
        ExportHandler<T> handler = handlerRegistry.getExportHandler(bizType);

        // 2. 查询数据
        List<T> dataList = handler.queryData(queryParam);

        // 3. 写入Excel
        return excelWriter.write(dataList, handler.getDtoClass(), handler.getColumns());
    }

    /**
     * 下载导入模板
     * @param bizType 业务类型
     * @return 模板文件字节数组
     */
    public <T> byte[] downloadTemplate(String bizType) {
        ImportHandler<T> handler = handlerRegistry.getImportHandler(bizType);
        return excelWriter.writeTemplate(handler.getColumns(), handler.getDtoClass());
    }
}

5.2 导入导出控制器

/**
 * 导入导出控制器
 * 统一的API入口
 */
@RestController
@RequestMapping("/api/import-export")
public class ImportExportController {

    @Autowired
    private ImportExportCenter importExportCenter;

    @Autowired
    private HandlerRegistry handlerRegistry;

    /**
     * 获取支持导入导出的业务类型列表
     */
    @GetMapping("/biz-types")
    public Result<List<BizTypeInfo>> getBizTypes() {
        return Result.success(handlerRegistry.getRegisteredBizTypes());
    }

    /**
     * 执行导入
     */
    @PostMapping("/import/{bizType}")
    public Result<ImportResult> doImport(
            @PathVariable String bizType,
            @RequestParam MultipartFile file) {
        ImportResult result = importExportCenter.doImport(bizType, file);
        return Result.success(result);
    }

    /**
     * 执行导出
     */
    @PostMapping("/export/{bizType}")
    public void doExport(
            @PathVariable String bizType,
            @RequestBody ExportQueryParam queryParam,
            HttpServletResponse response) throws IOException {
        byte[] bytes = importExportCenter.doExport(bizType, queryParam);

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition",
            "attachment; filename=" + URLEncoder.encode(bizType + ".xlsx", "UTF-8"));
        response.getOutputStream().write(bytes);
    }

    /**
     * 下载导入模板
     */
    @GetMapping("/template/{bizType}")
    public void downloadTemplate(
            @PathVariable String bizType,
            HttpServletResponse response) throws IOException {
        byte[] bytes = importExportCenter.downloadTemplate(bizType);

        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setHeader("Content-Disposition",
            "attachment; filename=" + URLEncoder.encode(bizType + "_template.xlsx", "UTF-8"));
        response.getOutputStream().write(bytes);
    }
}

六、业务模块实现示例

6.1 用户导入Handler

/**
 * 用户导入处理器
 * 处理用户数据的Excel导入
 */
@Component
public class UserImportHandler implements ImportHandler<UserImportDTO> {

    @Autowired
    private UserService userService;

    @Override
    public String getBizType() {
        return "user";
    }

    @Override
    public Class<UserImportDTO> getDtoClass() {
        return UserImportDTO.class;
    }

    @Override
    public List<ImportColumn> getColumns() {
        return List.of(
            ImportColumn.builder().name("用户名").fieldName("username")
                .required(true).fieldType(String.class).maxLength(20).build(),
            ImportColumn.builder().name("姓名").fieldName("realName")
                .required(true).fieldType(String.class).maxLength(10).build(),
            ImportColumn.builder().name("手机号").fieldName("phone")
                .required(true).fieldType(String.class).pattern("^1[3-9]\d{9}$").build(),
            ImportColumn.builder().name("邮箱").fieldName("email")
                .required(false).fieldType(String.class).build(),
            ImportColumn.builder().name("性别").fieldName("gender")
                .required(false).fieldType(String.class).dictCode("sys_gender").build(),
            ImportColumn.builder().name("部门").fieldName("deptName")
                .required(false).fieldType(String.class).build()
        );
    }

    @Override
    public ImportValidateResult validate(List<UserImportDTO> dataList) {
        List<ValidateError> errors = new ArrayList<>();

        for (int i = 0; i < dataList.size(); i++) {
            UserImportDTO dto = dataList.get(i);

            // 用户名唯一性校验
            if (userService.existsByUsername(dto.getUsername())) {
                errors.add(ValidateError.builder()
                    .rowIndex(i + 1)
                    .fieldName("username")
                    .message("用户名已存在: " + dto.getUsername())
                    .build());
            }

            // 手机号唯一性校验
            if (userService.existsByPhone(dto.getPhone())) {
                errors.add(ValidateError.builder()
                    .rowIndex(i + 1)
                    .fieldName("phone")
                    .message("手机号已存在: " + dto.getPhone())
                    .build());
            }
        }

        return ImportValidateResult.builder()
            .passed(errors.isEmpty())
            .errors(errors)
            .build();
    }

    @Override
    public ImportResult persist(List<UserImportDTO> dataList) {
        List<ValidateError> failDetails = new ArrayList<>();
        int successCount = 0;

        for (int i = 0; i < dataList.size(); i++) {
            try {
                userService.createUser(dataList.get(i));
                successCount++;
            } catch (Exception e) {
                failDetails.add(ValidateError.builder()
                    .rowIndex(i + 1)
                    .message("保存失败: " + e.getMessage())
                    .build());
            }
        }

        return ImportResult.builder()
            .totalCount(dataList.size())
            .successCount(successCount)
            .failCount(failDetails.size())
            .failDetails(failDetails)
            .build();
    }

    @Override
    public void afterImport(ImportResult result) {
        // 导入完成后刷新用户缓存
        log.info("用户导入完成: 成功{}, 失败{}", result.getSuccessCount(), result.getFailCount());
    }
}

6.2 用户导出Handler

/**
 * 用户导出处理器
 * 处理用户数据的Excel导出
 */
@Component
public class UserExportHandler implements ExportHandler<UserExportDTO> {

    @Autowired
    private UserService userService;

    @Override
    public String getBizType() {
        return "user";
    }

    @Override
    public Class<UserExportDTO> getDtoClass() {
        return UserExportDTO.class;
    }

    @Override
    public List<ExportColumn> getColumns() {
        return List.of(
            ExportColumn.builder().name("用户名").fieldName("username").build(),
            ExportColumn.builder().name("姓名").fieldName("realName").build(),
            ExportColumn.builder().name("手机号").fieldName("phone").build(),
            ExportColumn.builder().name("邮箱").fieldName("email").build(),
            ExportColumn.builder().name("性别").fieldName("genderName").build(),
            ExportColumn.builder().name("部门").fieldName("deptName").build(),
            ExportColumn.builder().name("状态").fieldName("statusName").build(),
            ExportColumn.builder().name("创建时间").fieldName("createTime")
                .dateFormat("yyyy-MM-dd HH:mm:ss").build()
        );
    }

    @Override
    public List<UserExportDTO> queryData(ExportQueryParam queryParam) {
        // 根据查询参数查询用户数据
        return userService.listExportData(queryParam);
    }

    @Override
    public String getFileName() {
        return "用户数据_" + LocalDate.now().format(DateTimeFormatter.ISO_DATE);
    }
}

七、完整导入流程

sequenceDiagram
    participant U as 用户
    participant C as Controller
    participant Center as ImportExportCenter
    participant R as HandlerRegistry
    participant H as UserImportHandler
    participant P as ExcelParser
    participant S as UserService

    U->>C: 上传Excel文件(bizType=user)
    C->>Center: doImport("user", file)
    Center->>R: getImportHandler("user")
    R-->>Center: UserImportHandler

    Center->>H: beforeImport()
    Center->>P: parse(file, UserImportDTO, columns)
    P-->>Center: List<UserImportDTO>

    Center->>H: validate(dataList)
    H->>S: existsByUsername/Phone
    S-->>H: 唯一性结果
    H-->>Center: ImportValidateResult

    alt 校验通过
        Center->>H: persist(validData)
        H->>S: createUser(dto)
        S-->>H: 保存结果
        H-->>Center: ImportResult
        Center->>H: afterImport(result)
        Center-->>C: ImportResult
        C-->>U: 导入成功
    else 校验失败
        Center-->>C: ImportResult(含错误详情)
        C-->>U: 导入失败,返回错误信息
    end

八、新增业务模块的扩展流程

flowchart TB
    A[新增业务模块
如: 商品管理] --> B[实现 ImportHandler]
    A --> C[实现 ExportHandler]

    B --> D["实现 getBizType()
返回 'product'"]
    D --> E[实现 getColumns()
定义导入列]
    E --> F[实现 validate()
数据校验逻辑]
    F --> G[实现 persist()
数据保存逻辑]

    C --> H["实现 getBizType()
返回 'product'"]
    H --> I[实现 getColumns()
定义导出列]
    I --> J[实现 queryData()
数据查询逻辑]

    G --> K[添加 @Component 注解]
    J --> K

    K --> L[Spring 自动注册到 HandlerRegistry]
    L --> M[导入导出中心自动支持
product 的导入导出]

    style M fill:#4CAF50,color:#fff

新增业务模块只需两步:

  1. 实现 Handler 接口:编写 ProductImportHandlerProductExportHandler
  2. 添加 @Component 注解:Spring 自动注册,无需修改任何已有代码
/**
 * 商品导入处理器 - 新增模块示例
 * 只需实现接口 + @Component 注解,即可自动注册
 */
@Component
public class ProductImportHandler implements ImportHandler<ProductImportDTO> {

    @Autowired
    private ProductService productService;

    @Override
    public String getBizType() {
        return "product";
    }

    @Override
    public Class<ProductImportDTO> getDtoClass() {
        return ProductImportDTO.class;
    }

    // ... 其他方法实现
}

九、Handler 与 Spring 依赖注入

Handler 作为 Spring Bean,可以自由注入其他服务,这是相比纯策略模式的重要优势:

flowchart TB
    subgraph Spring 容器
        A[HandlerRegistry] -->|自动扫描| B[所有 @Component Handler]

        subgraph UserImportHandler
            C[注入 UserService]
            D[注入 DictService]
            E[注入 DeptService]
        end

        subgraph RoleImportHandler
            F[注入 RoleService]
            G[注入 PermissionService]
        end
    end

    style A fill:#4CAF50,color:#fff
@Component
public class UserImportHandler implements ImportHandler<UserImportDTO> {

    // Handler 中可以自由注入需要的业务服务
    @Autowired
    private UserService userService;

    @Autowired
    private DictService dictService;    // 字典转换

    @Autowired
    private DeptService deptService;    // 部门名称→ID转换

    @Autowired
    private CacheService cacheService;  // 缓存操作

    // 在 validate/persist 方法中使用这些服务
}

结论与建议

核心要点

  1. 接口驱动解耦ImportHandler / ExportHandler 接口定义了导入导出的标准契约,业务模块只关心自己的逻辑
  2. 注册中心自动发现:通过 Spring ApplicationContextAware 自动扫描和注册 Handler,零配置
  3. 开闭原则:新增业务模块只需实现接口 + @Component,无需修改导入导出中心代码
  4. Spring 依赖注入:Handler 作为 Spring Bean,可以自由注入业务服务,实现复杂业务逻辑

最佳实践建议

建议 说明
bizType 全局唯一 使用模块名作为 bizType,避免冲突
校验与持久化分离 validate 只做校验,persist 只做保存,职责清晰
通用校验前置 通用校验(必填、格式、长度)由框架处理,业务校验由 Handler 处理
导入模板自动生成 根据 getColumns() 自动生成模板,避免手动维护
导入结果详细 返回每行的成功/失败状态,方便用户定位问题
大数据量分批 导入数据量大时,分批校验和持久化,避免内存溢出
异步导入支持 大文件导入使用异步任务,避免接口超时

相关资源