一佳技术

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 补齐