easy-notification 通知中间件设计
easy-notification 是独立部署的实时通知中间件(fatjar,自带 main 入口),基于 easy-socketio 组件构建。组件层提供服务器装配、握手鉴权 SPI、三级房间协议、Redis 分布式房间;本中间件只包含业务层——推送门面
NotificationPusher与 chat 事件示例。
一、背景与重写动机
旧版 easy-notification(io.matrix.pushing,netty-socketio 1.7.19)存在三类问题:
| 问题 | 说明 |
|---|---|
| netty 版本冲突 | 与 easy-socketio(netty-socketio 2.0.14 / netty 4.1.130)同仓冲突,被迫移出构建 |
| 基础设施重复 | 自带 ServerAdapter / BaseHandler / RedisStore 全套副本,与组件层重复演进 |
| 定位模糊 | 既是库(spring.factories)又无启动入口,spring.factories 注册的类路径错误从未生效 |
重写后:基础设施全部下沉到 easy-socketio,中间件回归”业务层 + 独立部署形态”,netty 冲突根源(1.7.19 直接依赖)消除。
二、定位与边界
定位:面向产品的实时通知中心服务——多端(Web / App)长连接接入、房间订阅、服务端推送。
| 做 | 不做 |
|---|---|
| Socket.IO 服务端托管(端口/生命周期/优雅停机) | 不实现长连接协议本身(easy-socketio/netty-socketio) |
| 推送 API(个人/产品房间/全员广播) | 不做消息持久化与离线补偿(属业务侧/后续 Event Store) |
| 集群横向扩容开关(Redis Pub/Sub 房间同步) | 不做跨进程推送桥接(见 §七 待办) |
| chat 事件处理器(示例业务) | 不做鉴权实现(AuthListener 由部署方提供 SPI) |
三、总体架构
flowchart TD
subgraph "easy-notification 业务层"
P1["NotificationPusher<br/>toUser / toProduct / broadcast / toRoom"]
C1["ChatEventHandler<br/>topic=chat 房间广播"]
A1["NotificationApplication<br/>main 入口 fatjar"]
A2["SocketioClusterConfiguration<br/>cluster.enabled 开关"]
end
subgraph "easy-socketio 组件层"
B1["ServerAdapter<br/>SmartLifecycle + FactoryBean"]
B2["AuthListener SPI<br/>握手鉴权 + canSubscribe"]
B3["RoomAdmin<br/>user:/product:/broadcast 三级房间"]
B4["RedisStoreFactory<br/>单机 Memory / 集群 Redis 自适应"]
end
subgraph "客户端"
K1["Web / App<br/>socket.io client EIO4"]
end
A1 --> B1
A2 --> B4
P1 -- "resolveNamespace + getRoomOperations" --> B3
C1 --> B1
B2 --> B3
K1 --> B1
B4 -- "cluster.enabled=true" --> R["Redis Pub/Sub"]
四、核心 API
4.1 NotificationPusher(服务端 → 客户端)
@Autowired NotificationPusher pusher;
pusher.toUser("u-1001", "order.paid", payload); // user:u-1001 个人房间
pusher.toProduct("PRD-001", "status", payload); // product:PRD-001 订阅房间
pusher.broadcast("notice", payload); // broadcast 全员
pusher.toRoom("custom-room", "event", payload); // 协议外自定义房间
内部统一经 ServerAdapter.resolveNamespace() 获取命名空间(规避 socket.io 默认命名空间 / 在 netty-socketio 内部名为空串的监听/投递错位陷阱),再向房间 sendEvent。
4.2 ChatEventHandler(客户端 → 服务端示例)
客户端 emit("chat", { room, nickname, content }) → 服务端向 room 房间广播同 topic 事件。私聊(receiver 定向)暂未实现,可经 toUser 补齐。
五、部署形态
# 构建 fatjar
mvn package -pl middlewares/easy-notification
# 单机
java -jar easy-notification.jar
# 集群(多实例 + Redis 房间同步)
java -jar easy-notification.jar \
--socketio.cluster.enabled=true \
--spring.data.redis.host=<redis-host>
无 web 容器(spring-boot-starter 不含 web),socketio.port 即服务端口。冒烟验证:Started NotificationApplication in 2.443s + SocketIO server started at port。
六、关键设计决策
| 决策 | 理由 |
|---|---|
废弃 operations handler(join/leave) | 与组件层 subscribe/unsubscribe 协议功能重复,且组件版多一道 canSubscribe 权限校验 |
删除 action 空实现 | TODO 空壳,无已知客户端 |
装配条件 socketio.enabled=true + @ConditionalOnBean(ServerAdapter) | 组件未启用时中间件 Bean 全部缺席,零影响 |
集群开关独立为 socketio.cluster.enabled | 单机部署不强制依赖 Redis 连接;开启才创建 RedisMessageListenerContainer |
包名 io.matrix.pushing → io.matrix.notification | 无消费方,重写时规范化;与模块名对齐 |
spring.factories 修正注册 | 旧版注册类路径错误(push.io.matrix.push.autoconfig.*)从未生效 |
七、已知问题
| 问题 | 严重度 | 说明 |
|---|---|---|
| 生产鉴权无默认实现 | 中 | fatjar 内无 AuthListener 实现,客户端只能 anonymous 进 broadcast 房间;需部署方提供实现(如复用 pulse AuthorizeService) |
| 跨进程推送未桥接 | 中 | NotificationPusher 是进程内 API;通知中心独立部署时,业务服务推送需经领域事件桥接(NotificationEvt → 中心订阅 → Pusher),待建 |
| 集群模式无自动化测试 | 中 | 依赖真实 Redis,暂靠人工验证(同 easy-socketio) |
| chat 私聊未实现 | 低 | 旧版即为 TODO,可经 toUser 补齐 |