55-字典缓存与通知SPI

55-字典缓存与通知SPI

两个小模块合成一篇:dict的computeIfAbsent缓存与notify的META-INF/services渠道注册------都是百行级代码,但各自代表一种经典模式。

文章目录

源码:

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修复。

相关推荐
Peregrine926 分钟前
C++ - > 类和对象
java·c++·算法
这个DBA有点耶28 分钟前
为什么UPDATE比SELECT危险得多?锁范围等于扫描行数
数据库·架构·代码规范
2501_9339232533 分钟前
SpringCloud组件:Eureka服务注册与发现
java·开发语言·后端·spring cloud·eureka
Wang's Blog35 分钟前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP 消息转换器
java·中间件·java-rabbitmq
Re_zero1 小时前
一次 SSE 断流排查:PrintWriter 吞掉了我的 IOException
java·后端
我是人✓1 小时前
Java核心语法:作用域、循环与大数值处理
java·开发语言·后端
夜之眷属2 小时前
PriorityQueue:for-each 遍历和 poll 取出,顺序一样吗?
java·服务器·数据结构·后端·算法
我不是阵雨2 小时前
分布式事务难题与实战解法全解析
分布式·微服务·架构
尤水就下2 小时前
🔥预热:在线文档的 block 架构是如何实现的(未完待续...)
前端·javascript·架构
Dawson Zhu2 小时前
《Agentic Design Patterns》第 13 章导读:人机协同(Human-in-the-Loop)
人工智能·语言模型·架构·aigc·agi