java每日精进 5.11【WebSocket】

1.纯Websocket实现消息发送

1.1一对一发送

前端

  1. 用户在输入框输入消息内容(sendText)

  2. 选择特定接收用户(sendUserId)

  3. 点击发送按钮触发handlerSend方法

  4. 构造消息内容JSON:

    复制代码
    {
      text: "Hello", // 消息内容
      toUserId: 123   // 目标用户ID
    }
  5. 包装为WebSocket标准格式:

    复制代码
    {
      type: "demo-message-send", // 消息类型
      content: '{"text":"Hello","toUserId":123}' // 字符串化的内容
    }
  6. 通过send()方法发送

  • 前端在setup函数中,使用useWebSocket方法,根据server变量(WebSocket 服务地址)建立连接。server地址由VITE_BASE_URL(环境变量)、/infra/ws路径和token(通过getRefreshToken获取)组成。
  • 设置autoReconnecttrue,表示自动重连;heartbeattrue,表示开启心跳机制。
  • 当用户在前端输入消息并点击发送按钮时,handlerSend函数被调用。
  • 首先将发送内容sendText和接收用户sendUserId进行 JSON 化处理,构建消息内容messageContent
  • 然后将消息类型typedemo-message-send)和消息内容messageContent再次 JSON 化,形成最终的消息jsonMessage
  • 最后使用send函数将jsonMessage发送到后端。
javascript 复制代码
const server = ref(
  (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
    '?token=' +
    getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:WebSocket 无法方便的刷新访问令牌
) // WebSocket 服务地址
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色

/** 发起 WebSocket 连接 */
const { status, data, send, close, open } = useWebSocket(server.value, {
  autoReconnect: true,
  heartbeat: true
})
javascript 复制代码
/** 发送消息 */
const sendText = ref('') // 发送内容
const sendUserId = ref('') // 发送人
const handlerSend = () => {
  // 1.1 先 JSON 化 message 消息内容
  const messageContent = JSON.stringify({
    text: sendText.value,
    toUserId: sendUserId.value
  })
  // 1.2 再 JSON 化整个消息
  const jsonMessage = JSON.stringify({
    type: 'demo-message-send',
    content: messageContent
  })
  // 2. 最后发送消息
  send(jsonMessage)
  sendText.value = ''
}

后端

  • 注册监听器DemoWebSocketMessageListener 类通过实现 WebSocketMessageListener<DemoSendMessage> 接口,并使用 @Component 注解将自己注册为 Spring Bean。框架启动时会扫描所有实现了该接口的 Bean,并将它们注册到消息处理器中。
java 复制代码
/**
 * WebSocket 示例:单发消息
 */
@Component
public class DemoWebSocketMessageListener implements WebSocketMessageListener<DemoSendMessage> {

    @Resource
    private WebSocketMessageSender webSocketMessageSender;

    @Override
    public void onMessage(WebSocketSession session, DemoSendMessage message) {
        Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session);
        // 情况一:单发
        if (message.getToUserId() != null) {
            DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
                    .setText(message.getText()).setSingle(true);
            webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToUserId(), // 给指定用户
                    "demo-message-receive", toMessage);
            return;
        }
        // 情况二:群发
        DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
                .setText(message.getText()).setSingle(false);
        webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), // 给所有用户
                "demo-message-receive", toMessage);
    }

    @Override
    public String getType() {
        return "demo-message-send";
    }

}
  • 消息类型绑定getType() 方法返回 "demo-message-send",这表明该监听器专门处理类型为 "demo-message-send" 的消息。当后端接收到消息时,会根据消息类型路由到对应的监听器。

当 WebSocket 服务器接收到消息后:

  1. 消息解析 :框架首先解析消息的 JSON 格式,提取 type 字段(如 "demo-message-send")。
  2. 类型匹配 :后端框架会自动将 type"demo-message-send" 的消息路由到 DemoWebSocketMessageListeneronMessage 方法。
  3. 调用回调 :将消息反序列化为 DemoSendMessage 对象,并调用监听器的 onMessage 方法。
java 复制代码
/**
 * JSON 格式 {@link WebSocketHandler} 实现类
 * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
 */
@Slf4j
public class JsonWebSocketMessageHandler extends TextWebSocketHandler {

    /**
     * type 与 WebSocketMessageListener 的映射
     * 用于存储不同消息类型对应的监听器,键为消息类型,值为对应的监听器实例
     */
    private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();

    @SuppressWarnings({"rawtypes", "unchecked"})
    public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
        // 遍历传入的监听器列表
        listenersList.forEach((Consumer<WebSocketMessageListener>)
                listener -> {
                    // 将监听器的类型(通过 getType() 方法获取)作为键,监听器实例作为值,存入 listeners 映射中
                    listeners.put(listener.getType(), listener);
                });
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1.1 空消息,跳过
        // 如果消息的负载长度为 0,说明是一个空消息,直接返回,不进行后续处理
        if (message.getPayloadLength() == 0) {
            return;
        }
        // 1.2 ping 心跳消息,直接返回 pong 消息。
        // 如果消息的负载长度为 4 且负载内容为 "ping",则向客户端发送 "pong" 消息,表示响应心跳
        if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
            session.sendMessage(new TextMessage("pong"));
            return;
        }

        // 2.1 解析消息
        try {
            // 将文本消息的负载解析为 JsonWebSocketMessage 对象
            JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
            // 如果解析后的消息为空,记录错误日志并返回,不进行后续处理
            if (jsonMessage == null) {
                log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
                return;
            }
            // 如果解析后的消息类型为空,记录错误日志并返回,不进行后续处理
            if (StrUtil.isEmpty(jsonMessage.getType())) {
                log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
                return;
            }
            // 2.2 获得对应的 WebSocketMessageListener
            // 根据消息类型从 listeners 映射中获取对应的监听器
            WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
            // 如果没有找到对应的监听器,记录错误日志并返回,不进行后续处理
            if (messageListener == null) {
                log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
                return;
            }
            // 2.3 处理消息
            // 获取监听器泛型参数类型
            Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
            // 将消息内容解析为对应类型的对象
            Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
            // 获取当前会话的租户 ID
            Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
            // 执行租户相关的操作,调用监听器的 onMessage 方法处理消息
            TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
        } catch (Throwable ex) {
            // 如果在处理消息过程中发生异常,记录错误日志
            log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
        }
    }

}

WebSocketMessageListener 之所以能监听消息,是因为:

  1. 接口契约:实现 WebSocketMessageListener 接口并指定消息类型(getType())。
  2. 框架支持:Spring 框架自动扫描并注册监听器,实现消息的解析和分发。
  3. 类型匹配:前端发送的消息 type 与后端监听器的 getType() 一致,触发回调。

这个过程类似于 HTTP 请求的路由机制,只不过 WebSocket 是长连接,需要持续监听消息。

通常,WebSocket 框架(如 Spring WebSocket)会提供以下核心组件:

  • 消息解码器 :将二进制数据转换为 Java 对象(如 DemoSendMessage)。
  • 消息路由器:根据消息类型将消息路由到对应的监听器。
  • 会话管理器 :维护所有 WebSocket 会话(WebSocketSession),并提供获取用户信息的工具(如 WebSocketFrameworkUtils.getLoginUserId)。
  • 后端的DemoWebSocketMessageListener类实现了WebSocketMessageListener接口的onMessage方法。
  • 当有消息到达时,onMessage方法被调用,从WebSocketSession中获取登录用户 ID(fromUserId)。
  • 根据消息中的toUserId判断是单发还是群发:
    • 如果toUserId不为空,则创建DemoReceiveMessage对象,设置fromUserIdtextsingletrue,通过webSocketMessageSendersendObject方法将消息发送给指定用户。
    • 如果toUserId为空,则创建DemoReceiveMessage对象,设置fromUserIdtextsinglefalse,通过webSocketMessageSendersendObject方法将消息发送给所有用户。
  1. JsonWebSocketMessageHandler接收并解析消息

  2. 根据type="demo-message-send"找到DemoWebSocketMessageListener

  3. 调用onMessage方法:

    • 从Session中获取发送者ID(fromUserId)

    • 检查message.getToUserId()不为null,进入单发逻辑

  4. 构造响应消息:

    复制代码
    new DemoReceiveMessage()
      .setFromUserId(fromUserId)
      .setText(message.getText())
      .setSingle(true)
  5. 通过webSocketMessageSender发送给指定用户:

    复制代码
    webSocketMessageSender.sendObject(
      UserTypeEnum.ADMIN.getValue(), // 用户类型
      message.getToUserId(),         // 目标用户ID
      "demo-message-receive",       // 消息类型
      toMessage                    // 消息内容
    )

实际示例:

  • 用户A(ID:100)发送"下午开会"给用户B(ID:101)

  • 前端发送:

    java 复制代码
    {"type":"demo-message-send","content":"{\"text\":\"下午开会\",\"toUserId\":101}"}
  • 后端处理后发送给用户B:

    复制代码
    {"type":"demo-message-receive","content":"{\"fromUserId\":100,\"text\":\"下午开会\",\"single\":true}"}

1.2一对多发送

前端

  1. 用户在输入框输入消息内容(sendText)

  2. 不选择特定用户(或选择"所有人")

  3. 点击发送按钮触发handlerSend方法

  4. 构造消息内容JSON:

    复制代码
    {
      text: "系统维护通知", // 消息内容
      toUserId: ""      // 空表示群发
    }
  5. 包装为WebSocket标准格式并发送

后端

  1. 同上接收解析流程

  2. onMessage方法中检查message.getToUserId()为null,进入群发逻辑

  3. 构造响应消息:

    复制代码
    new DemoReceiveMessage()
      .setFromUserId(fromUserId)
      .setText(message.getText())
      .setSingle(false)
  4. 通过webSocketMessageSender发送给所有用户:

    复制代码
    webSocketMessageSender.sendObject(
      UserTypeEnum.ADMIN.getValue(), // 用户类型
      "demo-message-receive",       // 消息类型
      toMessage                    // 消息内容
    )

实际示例:

  • 管理员发送"系统即将升级"给所有用户

  • 前端发送:

    java 复制代码
    {"type":"demo-message-send","content":"{\"text\":\"系统即将升级\",\"toUserId\":\"\"}"}

2.总结及类补充

后端代码

配置类:

  • YudaoWebSocketAutoConfiguration: 配置 WebSocket 端点、拦截器、会话管理和消息发送器,支持多种发送类型(local, redis, rocketmq, rabbitmq, kafka)。
  • 条件注解 @ConditionalOnProperty 允许通过配置启用/禁用 WebSocket 或切换发送类型。
  • 注册 WebSocketConfigurer、HandshakeInterceptor、WebSocketHandler 和 WebSocketSessionManager。
复制代码
 ```java
 /**
  * WebSocket 自动配置类
  * 负责 WebSocket 服务的初始化和相关组件的注册
  */
 @AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) 
 // 在 YudaoRedisMQConsumerAutoConfiguration 之前加载,确保 RedisWebSocketMessageConsumer 先创建
 @EnableWebSocket // 启用 Spring WebSocket 支持
 @ConditionalOnProperty(prefix = "moyun.websocket", value = "enable", matchIfMissing = true) 
 // 通过配置项 moyun.websocket.enable 控制是否启用 WebSocket,默认启用
 @EnableConfigurationProperties(WebSocketProperties.class) // 启用 WebSocket 配置属性类
 public class YudaoWebSocketAutoConfiguration {

     /**
      * 配置 WebSocket 处理器和握手拦截器
      */
     @Bean
     public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
                                                    WebSocketHandler webSocketHandler,
                                                    WebSocketProperties webSocketProperties) {
         return registry -> registry
                 // 注册 WebSocket 处理器并指定连接路径
                 .addHandler(webSocketHandler, webSocketProperties.getPath())
                 // 添加握手拦截器,用于验证和预处理
                 .addInterceptors(handshakeInterceptors)
                 // 允许所有域名跨域访问,否则前端连接会被阻止
                 .setAllowedOriginPatterns("*");
     }

     /**
      * 创建握手拦截器,用于在 WebSocket 握手阶段进行用户认证和权限检查
      */
     @Bean
     public HandshakeInterceptor handshakeInterceptor() {
         return new LoginUserHandshakeInterceptor();
     }

     /**
      * 创建 WebSocket 消息处理器
      * 包装 JsonWebSocketMessageHandler 并添加会话管理功能
      */
     @Bean
     public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
                                              List<? extends WebSocketMessageListener<?>> messageListeners) {
         // 1. 创建消息处理器,负责消息类型路由和分发
         JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
         // 2. 包装消息处理器,添加会话管理功能(如连接建立和关闭时的回调)
         return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
     }

     /**
      * 创建 WebSocket 会话管理器,用于管理所有活动的 WebSocket 会话
      */
     @Bean
     public WebSocketSessionManager webSocketSessionManager() {
         return new WebSocketSessionManagerImpl();
     }

     /**
      * 创建 WebSocket 请求授权自定义器,用于配置安全规则
      */
     @Bean
     public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
         return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
     }

     // ==================== 消息发送器配置 ====================

     /**
      * 本地模式消息发送器配置(单节点部署)
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "local")
     public class LocalWebSocketMessageSenderConfiguration {

         @Bean
         public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
             return new LocalWebSocketMessageSender(sessionManager);
         }

     }

     /**
      * Redis 模式消息发送器配置(分布式部署)
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "redis")
     public class RedisWebSocketMessageSenderConfiguration {

         @Bean
         public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
                                                                        RedisMQTemplate redisMQTemplate) {
             return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
         }

         @Bean
         public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
                 RedisWebSocketMessageSender redisWebSocketMessageSender) {
             return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
         }

     }

     /**
      * RocketMQ 模式消息发送器配置
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "rocketmq")
     public class RocketMQWebSocketMessageSenderConfiguration {

         @Bean
         public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
                 WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
                 @Value("${moyun.websocket.sender-rocketmq.topic}") String topic) {
             return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
         }

         @Bean
         public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
                 RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
             return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
         }

     }

     /**
      * RabbitMQ 模式消息发送器配置
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "rabbitmq")
     public class RabbitMQWebSocketMessageSenderConfiguration {

         @Bean
         public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
                 WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
                 TopicExchange websocketTopicExchange) {
             return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
         }

         @Bean
         public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
                 RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
             return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
         }

         /**
          * 创建 RabbitMQ 主题交换机,用于消息广播
          */
         @Bean
         public TopicExchange websocketTopicExchange(@Value("${moyun.websocket.sender-rabbitmq.exchange}") String exchange) {
             return new TopicExchange(exchange,
                     true,  // durable: 持久化交换机,重启后不丢失
                     false); // exclusive: 非排他性,允许多个连接使用
         }

     }

     /**
      * Kafka 模式消息发送器配置
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "kafka")
     public class KafkaWebSocketMessageSenderConfiguration {

         @Bean
         public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
                 WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate,
                 @Value("${moyun.websocket.sender-kafka.topic}") String topic) {
             return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
         }

         @Bean
         public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
                 KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
             return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
         }

     }

 }
 ```

认证与拦截:

  • TokenAuthenticationFilter: 解析 WebSocket URL 中的 token 参数,验证用户身份,构建 LoginUser 并存储到 Spring Security 上下文中。
复制代码
 ```java
 /**
  * Token 过滤器,验证 token 的有效性
  * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
  */
 @RequiredArgsConstructor
 public class TokenAuthenticationFilter extends OncePerRequestFilter {

     private final SecurityProperties securityProperties;

     private final GlobalExceptionHandler globalExceptionHandler;

     private final OAuth2TokenApi oauth2TokenApi;

     @Override
     @SuppressWarnings("NullableProblems")
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
             throws ServletException, IOException {
         String token = SecurityFrameworkUtils.obtainAuthorization(request,
                 securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
         if (StrUtil.isNotEmpty(token)) {
             Integer userType = WebFrameworkUtils.getLoginUserType(request);
             try {
                 // 1.1 基于 token 构建登录用户
                 LoginUser loginUser = buildLoginUserByToken(token, userType);
                 // 1.2 模拟 Login 功能,方便日常开发调试
                 if (loginUser == null) {
                     loginUser = mockLoginUser(request, token, userType);
                 }

                 // 2. 设置当前用户
                 if (loginUser != null) {
                     SecurityFrameworkUtils.setLoginUser(loginUser, request);
                 }
             } catch (Throwable ex) {
                 CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
                 ServletUtils.writeJSON(response, result);
                 return;
             }
         }

         // 继续过滤链
         chain.doFilter(request, response);
     }

     private LoginUser buildLoginUserByToken(String token, Integer userType) {
         try {
             OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token);
             if (accessToken == null) {
                 return null;
             }
             // 用户类型不匹配,无权限
             // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
             // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
             if (userType != null
                     && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
                 throw new AccessDeniedException("错误的用户类型");
             }
             // 构建登录用户
             return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
                     .setInfo(accessToken.getUserInfo()) // 额外的用户信息
                     .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes())
                     .setExpiresTime(accessToken.getExpiresTime());
         } catch (ServiceException serviceException) {
             // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
             return null;
         }
     }

     /**
      * 模拟登录用户,方便日常开发调试
      *
      * 注意,在线上环境下,一定要关闭该功能!!!
      *
      * @param request 请求
      * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号
      * @param userType 用户类型
      * @return 模拟的 LoginUser
      */
     private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) {
         if (!securityProperties.getMockEnable()) {
             return null;
         }
         // 必须以 mockSecret 开头
         if (!token.startsWith(securityProperties.getMockSecret())) {
             return null;
         }
         // 构建模拟用户
         Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
         return new LoginUser().setId(userId).setUserType(userType)
                 .setTenantId(WebFrameworkUtils.getTenantId(request));
     }

 }
 ```
  • LoginUserHandshakeInterceptor: 在 WebSocket 握手阶段将 LoginUser 存入 WebSocketSession 的 attributes。
复制代码
 ```java
 /**
  * 登录用户的 {@link HandshakeInterceptor} 实现类
  *
  * 流程如下:
  * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
  * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
  */
 public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {

     @Override
     public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                    WebSocketHandler wsHandler, Map<String, Object> attributes) {
         LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
         if (loginUser != null) {
             WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
         }
         return true;
     }

     @Override
     public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                WebSocketHandler wsHandler, Exception exception) {
         // do nothing
     }

 }
 ```

会话管理:

  • WebSocketSessionHandlerDecorator: 装饰 WebSocketHandler,在连接建立/关闭时管理 WebSocketSession。
复制代码
 ```java
 /**
  * {@link WebSocketHandler} 的装饰类,实现了以下功能:
  *
  * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
  * 2. 封装 {@link WebSocketSession} 支持并发操作
  */
 public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {

     /**
      * 发送时间的限制,单位:毫秒
      * 这里定义了发送消息的时间限制为 5 秒(1000 毫秒 * 5),用于控制消息发送的时间范围,
      * 可能是为了防止长时间的消息发送操作,避免资源占用或其他潜在问题。
      */
     private static final Integer SEND_TIME_LIMIT = 1000 * 5;
     /**
      * 发送消息缓冲上限,单位:bytes
      * 定义了发送消息的缓冲大小上限为 1024 * 100 字节,用于限制消息缓冲的大小,
      * 防止缓冲过大导致内存占用过多等问题。
      */
     private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;

     // WebSocket 会话管理器,用于管理 WebSocket 会话
     private final WebSocketSessionManager sessionManager;

     /**
      * 构造函数,接收被装饰的 WebSocketHandler 和 WebSocketSessionManager
      *
      * @param delegate      被装饰的 WebSocketHandler
      * @param sessionManager WebSocket 会话管理器
      */
     public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
                                             WebSocketSessionManager sessionManager) {
         // 调用父类构造函数,传入被装饰的 WebSocketHandler
         super(delegate);
         // 初始化会话管理器
         this.sessionManager = sessionManager;
     }

     /**
      * 当 WebSocket 连接建立时调用此方法
      *
      * @param session WebSocket 会话
      */
     @Override
     public void afterConnectionEstablished(WebSocketSession session) {
         // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
         // 使用定义的时间限制和缓冲大小限制,创建一个支持并发的 WebSocketSession 装饰类实例
         session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
         // 将新的会话添加到 WebSocketSessionManager 中进行管理
         sessionManager.addSession(session);
     }

     /**
      * 当 WebSocket 连接关闭时调用此方法
      *
      * @param session    WebSocket 会话
      * @param closeStatus 关闭状态
      */
     @Override
     public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
         // 从 WebSocketSessionManager 中移除对应的会话,完成连接关闭时的会话管理操作
         sessionManager.removeSession(session);
     }
 }
 ```
  • WebSocketSessionManager 和 WebSocketSessionManagerImpl:管理会话,支持按用户类型和 ID 查询。
复制代码
 ```java
 /**
  * {@link WebSocketSession} 管理器的接口
  */
 public interface WebSocketSessionManager {

     /**
      * 添加 Session
      *
      * @param session Session
      */
     void addSession(WebSocketSession session);

     /**
      * 移除 Session
      *
      * @param session Session
      */
     void removeSession(WebSocketSession session);

     /**
      * 获得指定编号的 Session
      *
      * @param id Session 编号
      * @return Session
      */
     WebSocketSession getSession(String id);

     /**
      * 获得指定用户类型的 Session 列表
      *
      * @param userType 用户类型
      * @return Session 列表
      */
     Collection<WebSocketSession> getSessionList(Integer userType);

     /**
      * 获得指定用户编号的 Session 列表
      *
      * @param userType 用户类型
      * @param userId 用户编号
      * @return Session 列表
      */
     Collection<WebSocketSession> getSessionList(Integer userType, Long userId);

 }
 ```

 ```java
 /**
  * 默认的 {@link WebSocketSessionManager} 实现类
  */
 public class WebSocketSessionManagerImpl implements WebSocketSessionManager {

     /**
      * id 与 WebSocketSession 映射
      *
      * key:Session 编号
      */
     private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();

     /**
      * user 与 WebSocketSession 映射
      *
      * key1:用户类型
      * key2:用户编号
      */
     private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions
             = new ConcurrentHashMap<>();

     @Override
     public void addSession(WebSocketSession session) {
         // 添加到 idSessions 中
         idSessions.put(session.getId(), session);
         // 添加到 userSessions 中
         LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
         if (user == null) {
             return;
         }
         ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
         if (userSessionsMap == null) {
             userSessionsMap = new ConcurrentHashMap<>();
             if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
                 userSessionsMap = userSessions.get(user.getUserType());
             }
         }
         CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
         if (sessions == null) {
             sessions = new CopyOnWriteArrayList<>();
             if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
                 sessions = userSessionsMap.get(user.getId());
             }
         }
         sessions.add(session);
     }

     @Override
     public void removeSession(WebSocketSession session) {
         // 移除从 idSessions 中
         idSessions.remove(session.getId());
         // 移除从 idSessions 中
         LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
         if (user == null) {
             return;
         }
         ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
         if (userSessionsMap == null) {
             return;
         }
         CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
         sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
         if (CollUtil.isEmpty(sessions)) {
             userSessionsMap.remove(user.getId(), sessions);
         }
     }

     @Override
     public WebSocketSession getSession(String id) {
         return idSessions.get(id);
     }

     @Override
     public Collection<WebSocketSession> getSessionList(Integer userType) {
         ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
         if (CollUtil.isEmpty(userSessionsMap)) {
             return new ArrayList<>();
         }
         LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
         Long contextTenantId = TenantContextHolder.getTenantId();
         for (List<WebSocketSession> sessions : userSessionsMap.values()) {
             if (CollUtil.isEmpty(sessions)) {
                 continue;
             }
             // 特殊:如果租户不匹配,则直接排除
             if (contextTenantId != null) {
                 Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
                 if (!contextTenantId.equals(userTenantId)) {
                     continue;
                 }
             }
             result.addAll(sessions);
         }
         return result;
     }

     @Override
     public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
         ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
         if (CollUtil.isEmpty(userSessionsMap)) {
             return new ArrayList<>();
         }
         CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);
         return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
     }

 }
 ```

消息处理:

  • JsonWebSocketMessageHandler:解析 JsonWebSocketMessage,根据 type 分发给 WebSocketMessageListener。
  • DemoWebSocketMessageListener: 处理 demo-message-send 类型的消息,支持单发和群发。
  • JsonWebSocketMessage:包含 type 和 content 字段。

消息发送:

  • WebSocketMessageSender: 定义消息发送接口。
  • AbstractWebSocketMessageSender: 实现消息发送逻辑,查询会话并发送 JsonWebSocketMessage。
  • LocalWebSocketMessageSender: 本地发送实现,适合单机场景。
  1. 配置websocket的Ss权限
java 复制代码
/**
 * WebSocket 的权限自定义
 * 负责为 WebSocket 端点的 HTTP 握手请求配置 Spring Security 权限,通过 permitAll() 确保握手请求不被阻止
 * 同时保留自定义的 token 认证逻辑(由 TokenAuthenticationFilter 和 LoginUserHandshakeInterceptor 处理)
 * 它解决了 Spring Security 对 WebSocket 握手请求的限制问题,是集成 Spring Security 的 WebSocket 功能的关键组件
 */
@RequiredArgsConstructor
public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {

    private final WebSocketProperties webSocketProperties;

    @Override
    public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
        registry.requestMatchers(webSocketProperties.getPath()).permitAll();
    }

}

前端代码

  • InfraWebSocket.vue: 使用 @vueuse/core 的 useWebSocket 建立连接,发送/接收 JsonWebSocketMessage,支持单发和群发消息。
  • 依赖用户列表 API(UserApi.getSimpleUserList)和 token 获取逻辑(getRefreshToken)。
相关推荐
子豪-中国机器人1 分钟前
C++ 信息学奥赛总复习题
java·jvm·算法
Java中文社群9 分钟前
Dify实战案例:MySQL查询助手!嘎嘎好用
java·人工智能·后端
程序猿阿伟13 分钟前
《深度探秘:Java构建Spark MLlib与TensorFlow Serving混合推理流水线》
java·spark-ml·tensorflow
TDengine (老段)21 分钟前
TDengine 开发指南—— UDF函数
java·大数据·数据库·物联网·数据分析·tdengine·涛思数据
键盘林24 分钟前
分布式系统简述
java·开发语言
可儿·四系桜25 分钟前
如何在 Java 中优雅地使用 Redisson 实现分布式锁
java·开发语言·分布式
一杯凉白开32 分钟前
硬件工程师口中的取低八位,中八位,高八位是什么意思?
android·网络协议
sszdzq40 分钟前
SpringCloudGateway 自定义局部过滤器
java·spring boot
消失的旧时光-19431 小时前
Android 开发中配置 USB 配件模式(Accessory Mode) 配件过滤器的配置
android·java
超级土豆粉1 小时前
JavaScript 标签加载
开发语言·javascript·ecmascript