1. 概述

本文将演示如何使用Spring WebSockets向特定会话或用户发送消息。关于基础概念可参考这篇入门文章

2. WebSocket配置

首先需要配置消息代理和WebSocket应用接口

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic/", "/queue/");
        config.setApplicationDestinationPrefixes("/app");
    }
     
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/greeting");
    }    
}

通过@EnableWebSocketMessageBroker我们启用了基于STOMP协议的WebSocket消息代理。STOMP(流式文本定向消息协议)是WebSocket上的消息传递标准。注意该注解需配合@Configuration使用。

虽然继承AbstractWebSocketMessageBrokerConfigurer不是必须的,但在这个简单示例中能更方便地自定义配置。

关键配置点:

  • ✅ 启用内存消息代理,处理/topic/queue前缀的消息
  • ✅ 注册STOMP接口/greeting
  • ⚠️ 如需启用SockJS支持,需修改注册代码:
registry.addEndpoint("/greeting").withSockJS();

3. 通过拦截器获取会话ID

获取会话ID的一种方式是添加Spring拦截器,在握手阶段从请求数据中提取信息。可直接在WebSocketConfig中添加:

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry
      .addEndpoint("/greeting")
      .setHandshakeHandler(new DefaultHandshakeHandler() {
          public boolean beforeHandshake(
            ServerHttpRequest request, 
            ServerHttpResponse response, 
            WebSocketHandler wsHandler,
            Map attributes) throws Exception {
              
              if (request instanceof ServletServerHttpRequest) {
                  ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
                  HttpSession session = servletRequest.getServletRequest().getSession();
                  attributes.put("sessionId", session.getId());
              }
              return true;
          }
      }).withSockJS();
}

4. WebSocket接口

从Spring 5.0.5.RELEASE开始,@SendToUser注解得到改进,无需额外配置即可向用户目的地发送消息。该注解现在支持通过/user/{sessionId}/...而非/user/{user}/...路由消息。

这意味着注解会自动依赖输入消息的会话ID,将回复发送到会话私有目的地:

@Controller
public class WebSocketController {

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    private Gson gson = new Gson();
 
    @MessageMapping("/message")
    @SendToUser("/queue/reply")
    public String processMessageFromClient(
      @Payload String message, 
      Principal principal) throws Exception {
        return gson
          .fromJson(message, Map.class)
          .get("name").toString();
    }
    
    @MessageExceptionHandler
    @SendToUser("/queue/errors")
    public String handleException(Throwable exception) {
        return exception.getMessage();
    }
}

关键点:

  • @SendToUser表示处理方法的返回值将作为消息发送到指定目的地
  • ✅ 目的地会自动添加/user/{username}前缀
  • ⚠️ 异常处理也使用相同机制路由错误消息

5. WebSocket客户端

function connect() {
    var socket = new WebSocket('ws://localhost:8080/greeting');
    ws = Stomp.over(socket);

    ws.connect({}, function(frame) {
        ws.subscribe("/user/queue/errors", function(message) {
            alert("Error " + message.body);
        });

        ws.subscribe("/user/queue/reply", function(message) {
            alert("Message " + message.body);
        });
    }, function(error) {
        alert("STOMP error " + error);
    });
}

function disconnect() {
    if (ws != null) {
        ws.close();
    }
    setConnected(false);
    console.log("Disconnected");
}

客户端实现要点:

  • ✅ 创建指向/greeting的WebSocket连接(对应配置中的接口映射)
  • ✅ 订阅/user/queue/errors/user/queue/reply
  • ⚠️ 虽然@SendToUser指定的是queue/errors,但实际消息会发送到/user/queue/errors

6. 总结

本文介绍了使用Spring WebSocket向特定用户或会话发送消息的完整方案。核心技巧是利用改进后的@SendToUser注解结合会话ID路由机制,避免了复杂的自定义配置。

完整示例代码可在GitHub仓库获取。


原始标题:A Quick Example of Spring Websockets' @SendToUser Annotation