SpringBoot中使用字典驱动的动态路由示例
一、什么是字典驱动
字典驱动(Table-Driven / Dictionary-Driven)是一种用数据配置 替代硬编码逻辑的编程方法。核心思想是把易变的业务规则或路由映射关系存储在数据库/配置表中,运行时通过查表获取实际值,避免每次新增场景都改代码。
典型场景:同一个业务动作,因来源、渠道、租户不同,需要调用不同的下游接口或走不同的处理逻辑。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、示例业务场景
业务背景
某生态商可能有多个来源平台(如 aaa、其他),不同平台对应不同的第三方 API 接入编码。通知生态商时需要根据"通知类型 + 来源平台"动态获取对应的 apiCode。
实现方式
java
// 1. 枚举定义通知类型前缀
public enum TestsyncNoticeTypeEnum {
RECEIVE("test_async_notice_receive_order_", "接单回调"),
OOSTOCK("test_async_notice_oostock_", "补货失败回调"),
CANCEL("test_async_notice_cancel_order_", "取消回调");
}
// 2. 运行时拼接字典Key = 枚举前缀 + 来源平台
String itemType = typeEnum.getCode() + sourceSystem;
// 例如: "test_async_notice_cancel_order_aaa"
// 3. 查字典表获取实际的apiCode
RestControllerResult<List<SearchSysDictByConditionResultDto>> dictResult =
baseFeign.listSysDictByItemTypes(Collections.singletonList(itemType));
String apiCode = dictResult.getData().get(0).getItemCode();
// 4. 用apiCode调用第三方
outOthersService.hmmCommon(apiCode, params, symbol);
新增来源平台时
只需在字典表中 INSERT 一行记录,不需要改代码、不需要发版。
三、核心知识点
3.1 表驱动法(Table-Driven Methods)
出自《代码大全》(Code Complete),核心原则:
当你发现代码中有大量 if-else 或 switch-case 根据某个变量值做不同处理时,考虑用表(Map/数据库/配置文件)来替代。
适用条件:
- 映射关系是一对一 或一对多的简单对应
- 映射关系频繁变化 或可预见会增长
- 映射逻辑不涉及复杂计算,只是值的查找
不适用场景:
- 不同分支有完全不同的复杂处理逻辑(此时应用策略模式)
- 映射关系固定不变且数量很少(直接 if-else 更清晰)
3.2 与策略模式的区别
| 维度 | 字典驱动 | 策略模式 |
|---|---|---|
| 解决的问题 | 值/配置的动态获取 | 行为/算法的动态切换 |
| 变化点 | 数据(URL、编码、参数) | 逻辑(不同的处理流程) |
| 扩展方式 | 加配置 | 加代码(新策略类) |
| 适合场景 | 调不同接口、发不同消息 | 不同计费规则、不同审批流程 |
| 复杂度 | 低 | 中 |
3.3 与 SPI 机制的区别
| 维度 | 字典驱动 | SPI |
|---|---|---|
| 发现方式 | 查数据库/配置 | 类路径扫描 |
| 颗粒度 | 值级别 | 实现类级别 |
| 热更新 | 支持(改数据库即生效) | 不支持(需重启) |
| 场景 | 简单路由映射 | 框架插件化扩展 |
四、通用实现方案
方案一:数据库字典表
最常见的实现方式,适合需要运行时动态修改的场景。
java
/**
* 通用字典路由服务.
*/
@Service
public class DictRouteService {
@Resource
private SysDictRepository sysDictRepository;
@Resource
private RedisTemplate<String, String> redisTemplate;
private static final String DICT_CACHE_PREFIX = "sys_dict:";
private static final long CACHE_EXPIRE_MINUTES = 30;
/**
* 根据字典类型和业务Key获取路由值.
*
* @param dictType 字典类型(如 "payment_channel")
* @param bizKey 业务Key(如 "alipay")
* @return 路由值(如接口地址、编码等)
*/
public String getRouteValue(String dictType, String bizKey) {
String cacheKey = DICT_CACHE_PREFIX + dictType + ":" + bizKey;
// 1. 先查缓存
String cachedValue = redisTemplate.opsForValue().get(cacheKey);
if (cachedValue != null) {
return cachedValue;
}
// 2. 缓存未命中,查数据库
SysDict dict = sysDictRepository.findByItemTypeAndItemCode(dictType, bizKey);
if (dict == null) {
throw new BusinessException("未找到路由配置, dictType=" + dictType + ", bizKey=" + bizKey);
}
// 3. 写入缓存
redisTemplate.opsForValue().set(cacheKey, dict.getItemValue(),
CACHE_EXPIRE_MINUTES, TimeUnit.MINUTES);
return dict.getItemValue();
}
/**
* 批量获取某类型下所有路由配置.
*/
public Map<String, String> getAllRoutes(String dictType) {
List<SysDict> dictList = sysDictRepository.findByItemType(dictType);
return dictList.stream()
.collect(Collectors.toMap(SysDict::getItemCode, SysDict::getItemValue));
}
/**
* 清除缓存(字典变更时调用).
*/
public void evictCache(String dictType, String bizKey) {
String cacheKey = DICT_CACHE_PREFIX + dictType + ":" + bizKey;
redisTemplate.delete(cacheKey);
}
}
字典表 DDL:
sql
CREATE TABLE sys_dict (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
item_type VARCHAR(100) NOT NULL COMMENT '字典类型',
item_code VARCHAR(100) NOT NULL COMMENT '字典编码',
item_value VARCHAR(500) NOT NULL COMMENT '字典值',
item_name VARCHAR(200) COMMENT '中文描述',
sort_order INT DEFAULT 0 COMMENT '排序',
status TINYINT DEFAULT 1 COMMENT '状态 1启用 0禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_type_code (item_type, item_code)
) COMMENT '系统字典表';
使用示例:
java
// 场景:不同支付渠道对应不同回调通知地址
String notifyUrl = dictRouteService.getRouteValue("payment_notify_url", "alipay");
paymentClient.setNotifyUrl(notifyUrl);
方案二:枚举 + Map 注册(无数据库依赖)
适合映射关系相对固定、不需要运行时修改的场景。
java
/**
* 消息渠道路由器.
* 根据消息类型动态选择发送渠道.
*/
public class MessageChannelRouter {
private static final Map<String, MessageChannelConfig> ROUTE_TABLE = new HashMap<>();
static {
ROUTE_TABLE.put("order_created", new MessageChannelConfig("sms", "SMS-001", "订单创建通知"));
ROUTE_TABLE.put("order_shipped", new MessageChannelConfig("push", "PUSH-002", "发货通知"));
ROUTE_TABLE.put("order_cancelled", new MessageChannelConfig("email", "EMAIL-003", "取消通知"));
ROUTE_TABLE.put("refund_success", new MessageChannelConfig("sms", "SMS-004", "退款成功通知"));
}
/**
* 获取消息发送渠道配置.
*/
public static MessageChannelConfig getChannel(String eventType) {
MessageChannelConfig config = ROUTE_TABLE.get(eventType);
if (config == null) {
throw new IllegalArgumentException("未配置的事件类型: " + eventType);
}
return config;
}
@Data
@AllArgsConstructor
public static class MessageChannelConfig {
private String channel; // sms / push / email
private String templateCode; // 模板编码
private String description; // 描述
}
}
// 使用
MessageChannelConfig config = MessageChannelRouter.getChannel("order_shipped");
messageService.send(config.getChannel(), config.getTemplateCode(), content);
方案三:Spring Bean + 注解注册(字典驱动 + 策略模式结合)
适合不同路由不仅值不同,处理逻辑也有差异的场景。
java
/**
* 路由标识注解.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouteHandler {
String value(); // 路由Key
}
/**
* 处理器接口.
*/
public interface OrderNotifyHandler {
void notify(String orderNo, String content);
}
/**
* ykt平台处理器.
*/
@Component
@RouteHandler("ykt")
public class YktOrderNotifyHandler implements OrderNotifyHandler {
@Override
public void notify(String orderNo, String content) {
// 调用ykt的接口
}
}
/**
* 其他平台处理器.
*/
@Component
@RouteHandler("other_platform")
public class OtherPlatformNotifyHandler implements OrderNotifyHandler {
@Override
public void notify(String orderNo, String content) {
// 调用其他平台的接口
}
}
/**
* 路由分发器 --- 自动收集所有标注了 @RouteHandler 的 Bean.
*/
@Component
public class OrderNotifyDispatcher implements ApplicationContextAware {
private final Map<String, OrderNotifyHandler> handlerMap = new HashMap<>();
@Override
public void setApplicationContext(ApplicationContext ctx) {
Map<String, Object> beans = ctx.getBeansWithAnnotation(RouteHandler.class);
beans.forEach((name, bean) -> {
RouteHandler annotation = bean.getClass().getAnnotation(RouteHandler.class);
handlerMap.put(annotation.value(), (OrderNotifyHandler) bean);
});
}
/**
* 根据平台标识路由到对应处理器.
*/
public void dispatch(String platform, String orderNo, String content) {
OrderNotifyHandler handler = handlerMap.get(platform);
if (handler == null) {
throw new BusinessException("未找到平台处理器: " + platform);
}
handler.notify(orderNo, content);
}
}
// 使用
orderNotifyDispatcher.dispatch("ykt", "SO.20260701.000020", jsonContent);
方案四:配置文件驱动(Nacos/Apollo)
适合微服务架构中需要集中管理且支持热更新的场景。
yaml
# nacos配置
route:
notify:
ykt:
api-code: "hmm-ykt-cancel-001"
url: "https://api.ykt.com/notify"
timeout: 5000
other:
api-code: "hmm-other-cancel-001"
url: "https://api.other.com/notify"
timeout: 3000
@Component
@ConfigurationProperties(prefix = "route.notify")
@RefreshScope // Nacos热更新
public class NotifyRouteConfig {
private Map<String, NotifyEndpoint> endpoints = new HashMap<>();
@Data
public static class NotifyEndpoint {
private String apiCode;
private String url;
private Integer timeout;
}
public NotifyEndpoint getEndpoint(String platform) {
return endpoints.get(platform);
}
}
五、各方案对比
| 方案 | 热更新 | 复杂度 | 适用场景 |
|---|---|---|---|
| 数据库字典表 | ✅ 改库即生效 | 低 | 简单值映射、频繁新增配置 |
| 枚举+Map | ❌ 需改代码 | 最低 | 映射固定、数量少 |
| 注解注册Bean | ❌ 需加代码 | 中 | 不同路由有不同处理逻辑 |
| 配置中心(Nacos) | ✅ 推送生效 | 中 | 微服务集中管理、含URL等连接信息 |
六、使用字典驱动时的注意事项
- 缓存策略:字典表数据变更频率低但查询频率高,必须加缓存(Redis/本地缓存),避免每次请求都查库
- 缺失处理:查不到配置时要明确报错(抛异常 + 日志),不能静默失败
- 兜底值:关键业务可设置默认值,配置缺失时用默认值兜底
- 变更通知:字典变更后需要清除缓存(可通过 MQ 广播通知各节点)
- 版本管理:重要配置变更建议记录变更历史,方便回溯