必须用convertandsendtouser+“/user/”订阅路径且用户标识绑定websocket session,因spring通过userdestinationmessagehandler将/user/{username}/queue/xxx路由至对应session,而convertandsend发往/topic/为广播、发往/user/路径则被丢弃。

直接结论:必须用 convertAndSendToUser + /user/ 订阅路径,且用户标识需绑定到 WebSocket Session,不能靠前端传用户名来路由。
为什么 convertAndSend 发不到指定用户
很多人试过 simpMessagingTemplate.convertAndSend("/topic/user123", msg),发现所有订阅 /topic/xxx 的客户端都收到了——这是对的,因为 /topic 是广播模式。Spring 的“发给某人”机制不依赖路径匹配,而是依赖用户会话绑定。真正起作用的是 /user/{username}/queue/xxx 这套约定,底层由 UserDestinationMessageHandler 解析并路由到对应 session ID。
-
/topic/xxx→ 所有在线订阅者(不管是谁) -
/user/{username}/queue/xxx→ 仅该用户当前所有 WebSocket 连接(一个用户可多端登录) - 如果只用
convertAndSend往/user/...发,消息会被丢弃:它不识别用户上下文
convertAndSendToUser 的三个必要参数怎么填
调用 simpMessagingTemplate.convertAndSendToUser 时,三个参数含义和常见错误如下:
- 第一个参数
username:必须是Principal.getName()返回的值,通常来自自定义Principal实现(如User类),不是 HTTP Session 里的 key,也不是数据库查出来的 nickname - 第二个参数
destination:必须以/queue/开头(不能是/topic/),例如"/queue/notify";最终客户端订阅的是/user/queue/notify - 第三个参数
message:任意对象,会经 Jackson 序列化为 JSON 字符串;注意不要传含循环引用的对象,否则抛JsonMappingException
示例调用:
simpMessagingTemplate.convertAndSendToUser("alice", "/queue/notify", Map.of("type", "msg", "content", "hello"));
前端订阅 /user/queue/xxx 为什么收不到消息
常见原因不是后端没发,而是前端没正确建立带身份的连接或订阅路径不匹配:
- WebSocket 连接 URL 必须带认证信息,比如通过 Cookie 携带登录态,否则 Spring 无法从
StompHeaders中提取Principal - 客户端订阅时写
stompClient.subscribe("/user/queue/notify", ...),不能漏掉/user/前缀,也不能写成/user/alice/queue/notify(Spring 会自动补全) - 如果用 SockJS,确保
withSockJS()已启用,且前端加载了sockjs-client和stompjs正确版本(v6+ 与 v5 API 不兼容) - 浏览器控制台检查 STOMP CONNECT 帧里是否包含
user-nameheader;没有就说明 Principal 未注入成功
用户登出或断连后消息还会发过去吗
不会,但要注意边界情况:
-
convertAndSendToUser是「尽力投递」:如果目标用户当前无活跃 session,消息直接丢弃,不报错也不重试 - Spring 不维护用户离线消息队列,如需离线推送,得自己用 Redis 缓存 + 登录时拉取
- 多个 tab 同时登录同一用户:消息会发到每个 tab 对应的 session,不是只发一次
- Session 销毁时机取决于
@OnClose是否被触发,若网络异常断开,可能延迟数秒才清理,期间仍可能收到消息
最容易被忽略的一点:convertAndSendToUser 的 username 必须和 Principal.getName() 完全一致——大小写、空格、编码都不能差,否则 Spring 找不到对应 session,静默失败。建议在登录成功后立刻打日志确认该值。











