Handler 注册机制业务与导入导出解耦
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
新增业务模块只需两步:
- 实现 Handler 接口:编写
ProductImportHandler和ProductExportHandler - 添加
@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 方法中使用这些服务
}
结论与建议
核心要点
- 接口驱动解耦:
ImportHandler/ExportHandler接口定义了导入导出的标准契约,业务模块只关心自己的逻辑 - 注册中心自动发现:通过 Spring
ApplicationContextAware自动扫描和注册 Handler,零配置 - 开闭原则:新增业务模块只需实现接口 +
@Component,无需修改导入导出中心代码 - Spring 依赖注入:Handler 作为 Spring Bean,可以自由注入业务服务,实现复杂业务逻辑
最佳实践建议
| 建议 | 说明 |
|---|---|
| bizType 全局唯一 | 使用模块名作为 bizType,避免冲突 |
| 校验与持久化分离 | validate 只做校验,persist 只做保存,职责清晰 |
| 通用校验前置 | 通用校验(必填、格式、长度)由框架处理,业务校验由 Handler 处理 |
| 导入模板自动生成 | 根据 getColumns() 自动生成模板,避免手动维护 |
| 导入结果详细 | 返回每行的成功/失败状态,方便用户定位问题 |
| 大数据量分批 | 导入数据量大时,分批校验和持久化,避免内存溢出 |
| 异步导入支持 | 大文件导入使用异步任务,避免接口超时 |