55-字典缓存与通知SPI
两个小模块合成一篇:dict的computeIfAbsent缓存与notify的META-INF/services渠道注册------都是百行级代码,但各自代表一种经典模式。
文章目录
- 55-字典缓存与通知SPI
-
- 一、DictService:Cache-Aside的教科书实现
- 二、Aa10实体的两个特殊处理
- [三、NotifyChannelManager:Java SPI](#三、NotifyChannelManager:Java SPI)
- 四、两个渠道实现
- 五、字典+通知在待办提醒里的协作
源码:
browise-dict/src/main/java/com/browise/dict/service/DictService.java(2.8KB)
browise-notify/src/main/java/com/browise/notify/channel/NotifyChannelManager.java(1.4KB)
一、DictService:Cache-Aside的教科书实现
java
@Service
public class DictService {
private final ConcurrentHashMap<String, List<DictItem>> cache = new ConcurrentHashMap<>();
private final Aa10Mapper aa10Mapper;
public List<DictItem> getItems(String codeName) {
// ①缓存命中直接返回
List<DictItem> items = cache.get(codeName);
if (items != null) return items;
// ②miss→查库→入缓存
items = aa10Mapper.selectByAaa100(codeName);
cache.put(codeName, items);
return items;
}
public String getLabel(String codeName, String value) {
// 码值翻译:"1"→"男"
return getItems(codeName).stream()
.filter(i -> i.getValue().equals(value))
.map(DictItem::getLabel).findFirst().orElse(value);
}
public void refresh(String codeName) {
cache.remove(codeName); // 失效------下次get重查
// 或全量:cache.clear()
}
public void saveItem(DictItem item) {
aa10Mapper.insert(item);
refresh(item.getAaa100()); // 写后失效------自己先看见
}
}
Cache-Aside三件套 ------读(miss回源)、写(先库后失效缓存)、强制失效(refresh)。computeIfAbsent的单行版也可以(第01篇提过browise-dict用它)------原子性更好但锁粒度大(同key并发查库只放一个进去)------两种写法在这个低频场景都成立。
为什么字典适合缓存 ------AA10码表读极频 (每个下拉框/每列表格的翻译)写极少(管理员偶尔加码值)------读多写少是缓存的第一判据。
不可变列表的细节 ------查询结果直接缓存引用------调用方拿到List如果add会污染缓存 。生产版该Collections.unmodifiableList()包一层------当前实现靠约定(没人去改字典列表)。
二、Aa10实体的两个特殊处理
第58篇bug记录里两条都关于Aa10:
java
@TableName("AA10")
public class Aa10 {
// 没有@TableId!------复合主键(AAA100+AAA102)MP不支持
// → updateById/deleteById不可用
// → 自定义@Update("UPDATE AA10 SET AAA103=#{label} WHERE AAA100=#{type} AND AAA102=#{code}")
}
无@TableId的实体 ------MP的BaseMapper大量方法依赖单主键------复合主键表只能手写SQL 。这是MyBatis-Plus的边界------政务标准表(劳动部99版数据字典)复合主键很常见------BaseEntity路径不够走原生Mapper。
DELETE的body问题 ------HTTP DELETE带body不符合部分前端/网关规范------改成@RequestParam query param (DELETE /api/codes/item?aaa100=x&aaa102=y)------第58篇的三条修复之一。
三、NotifyChannelManager:Java SPI
java
public class NotifyChannelManager {
private final Map<String, NotifyChannel> channels = new ConcurrentHashMap<>();
public NotifyChannelManager() {
// ServiceLoader扫描META-INF/services
ServiceLoader<NotifyChannel> loader = ServiceLoader.load(NotifyChannel.class);
for (NotifyChannel ch : loader) {
channels.put(ch.getId(), ch);
}
}
public void send(String channelId, String to, String msg) {
NotifyChannel ch = channels.get(channelId);
if (ch != null) ch.send(to, msg);
}
}
META-INF/services注册 ------每个渠道jar的META-INF/services/com.browise.notify.channel.NotifyChannel文件写实现类全名------ServiceLoader运行时发现。
与第39篇拦截器的Spring版对照:
| 拦截器链(Spring getBeansOfType) | 通知渠道(Java SPI) | |
|---|---|---|
| 注册方式 | @Component自动 | META-INF/services文件 |
| 依赖 | Spring容器 | 无------纯JDK |
| 适用 | 应用内扩展 | 跨jar/独立部署 |
notify为什么选Java SPI ------通知渠道可能是独立jar (阿里云SMS SDK封装)------不依赖Spring的模块更轻------demo应用引jar即得渠道,不引就没有。
四、两个渠道实现
WorkNoticeChannel(5.6KB)------政务协同办公平台的HTTP通知------纯POST。
AliyunSmsChannel (5.8KB)------短信------HMAC-SHA1签名 + 降级策略:
发短信
→ 签名计算
→ HTTP调阿里云
→ 失败 → 降级:写本地待发表 → 定时任务重试
降级的存在理由 ------短信网关的可用性不能绑架业务------通知发不出不该阻断主流程(与第43篇推送失败的尽力而为同理------通知是增强不是依赖)。
五、字典+通知在待办提醒里的协作
任务逾期(ActPrcday配置到期------第42篇)
→ 定时任务扫描
→ NotifyService.send("workNotice", 处理人, "您有待办逾期")
→ NotifyChannelManager路由到WorkNoticeChannel
→ HTTP推政务平台
→ 前端消息中心同时可见(WORKFLOW_MESSAGE表)
字典服务给所有展示层翻译码值、通知服务把事件送达渠道------两个"周边"模块托着所有业务模块的公共需求。
✅ 亮点:Cache-Aside三件套与不可变列表细节、复合主键表的MP边界、SPI与Spring注册的跨jar适用性对照、短信渠道的降级策略、字典+通知在逾期提醒的协作链。适合做公共组件的人。扩展方向:第39篇Spring注册对照、第42篇时限配置、第58篇bug修复。