Spring WebSocket无客户端请求向订阅客户端推送通知问题咨询
问题1:SimpMessagingTemplate调用空指针修复
空指针核心原因是依赖注入写法错误:你在Controller里加@Autowired的方法名是WebSocketController,既不是类的构造方法,也不符合Java Bean setter方法命名规范(setXxx格式),Spring容器不会执行该方法完成注入,messagingTemplate字段始终为null,调用时必然触发空指针。
另外你的接口写法存在参数绑定错误:@RequestMapping定义的路径是/post/message,没有配置路径占位符,却用@PathVariable接收参数,请求会直接报400错误。
修正后的Controller代码如下:
@RestController public class WebSocketController { private final SimpMessagingTemplate messagingTemplate; // Spring 4.3+版本构造方法自动注入,不需要额外加@Autowired public WebSocketController(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } @PostMapping("/post/message") // 路径传参就把路径改成/post/message/{message},否则用@RequestParam/@RequestBody接参 public void postMessage(@RequestParam String message) { // 广播推送所有订阅了/topic/myDest的客户端 messagingTemplate.convertAndSend("/topic/myDest", message); } }
注意:确保你的WebSocket配置类已经加了@EnableWebSocketMessageBroker注解,且正确配置了消息代理,否则SimpMessagingTemplate本身不会被初始化。基础配置示例:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry config) { // 启用内存简单代理,支持广播/topic、点对点/user前缀的目的地 config.enableSimpleBroker("/topic", "/user"); config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 前端连接端点,允许跨域 registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS(); } }
问题2:客户端参数存储与定向推送实现
你已经通过入站通道拦截器拿到了连接、订阅、断开的生命周期事件,完全可以基于这个能力存储客户端关联参数,实现无客户端请求时的主动定向推送,方案如下:
- 用线程安全的
ConcurrentHashMap作为客户端会话缓存,Key存WebSocket会话ID,Value存需要绑定的业务参数(比如用户ID、客户端标识、订阅的目的地列表等) - 在
CONNECT事件触发时,从STOMP帧头中取出客户端传入的自定义参数,和sessionId绑定存入缓存 - 在
SUBSCRIBE事件触发时,取出当前会话订阅的目的地,更新到对应缓存条目里 - 在
DISCONNECT事件触发时,主动删除缓存中对应sessionId的条目,避免内存泄漏
补充:前端建立STOMP连接时,可以在connectHeaders中传入自定义参数(比如userId、客户端标识),后端可以直接通过accessor.getFirstNativeHeader("参数名")获取。
带会话存储的拦截器代码示例:
// 线程安全的会话缓存,也可以单独抽成Spring管理的Bean方便全局业务调用 private final ConcurrentHashMap<String, ClientSessionInfo> sessionCache = new ConcurrentHashMap<>(); @Override public void configureClientInboundChannel(ChannelRegistration registration){ registration.interceptors(new ChannelInterceptor() { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); // 判空避免空指针 if (accessor == null || accessor.getCommand() == null) { return message; } String sessionId = accessor.getSessionId(); switch (accessor.getCommand()) { case CONNECT: // 从连接头中取客户端传的自定义参数 String userId = accessor.getFirstNativeHeader("userId"); ClientSessionInfo info = new ClientSessionInfo(); info.setSessionId(sessionId); info.setUserId(userId); info.setConnectTime(System.currentTimeMillis()); sessionCache.put(sessionId, info); System.out.println("客户端连接,sessionId:" + sessionId + ",userId:" + userId); // 连接建立成功后可直接主动推消息,不需要等客户端发请求 messagingTemplate.convertAndSendToUser(sessionId, "/queue/notice", "连接成功,欢迎使用"); break; case SUBSCRIBE: // 记录当前客户端订阅的目的地 String subscribeDest = accessor.getDestination(); sessionCache.computeIfPresent(sessionId, (k, v) -> { v.getSubscribeDests().add(subscribeDest); return v; }); System.out.println("客户端订阅,sessionId:" + sessionId + ",订阅目的地:" + subscribeDest); break; case DISCONNECT: // 连接断开移除缓存 sessionCache.remove(sessionId); System.out.println("客户端断开连接,sessionId:" + sessionId); break; default: break; } return message; } }); } // 自定义客户端会话信息类,可按业务需求增减字段 class ClientSessionInfo { private String sessionId; private String userId; private Long connectTime; private List<String> subscribeDests = new ArrayList<>(); // 自行补充getter、setter方法,集成Lombok可直接加@Data注解省略 public String getSessionId() {return sessionId;} public void setSessionId(String sessionId) {this.sessionId = sessionId;} public String getUserId() {return userId;} public void setUserId(String userId) {this.userId = userId;} public Long getConnectTime() {return connectTime;} public void setConnectTime(Long connectTime) {this.connectTime = connectTime;} public List<String> getSubscribeDests() {return subscribeDests;} public void setSubscribeDests(List<String> subscribeDests) {this.subscribeDests = subscribeDests;} }
后续需要主动推送时,直接遍历sessionCache拿到对应客户端的sessionId或者业务标识,调用messagingTemplate.convertAndSendToUser()方法即可定向推送,不需要客户端主动发起请求。如果是广播消息,直接调用convertAndSend()往/topic前缀的目的地发即可,所有订阅了该目的地的在线客户端都能收到。
内容的提问来源于stack exchange,提问作者Sam99

