You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 15:27:14