mcpField
S
spring-websocket

spring-websocket

github.com/claude-dev-suite开发4639 次安装2 天前更新
代码仓库

关于 spring-websocket

面向使用 Spring 构建实时消息功能的开发者的 WebSocket 技能,展示 STOMP 消息代理、控制器、事件处理和安全配置。还涵盖心跳、错误处理及外部消息代理的扩展参考。

主要功能

  1. 01STOMP 消息代理配置
  2. 02消息控制器与事件处理
  3. 03WebSocket 安全配置
  4. 04会话属性与错误处理
  5. 05心跳和外部代理参考

适用场景

  1. 01在 Spring 应用中构建聊天功能
  2. 02处理客户端连接与消息事件
  3. 03规划 WebSocket 安全和代理配置

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill spring-websocket

安装后在 Claude Code 中输入/spring-websocket确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

advanced.md
brokers.md
security.md

详细介绍

Spring WebSocket

快速入门


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // Prefix for messages from server to clients (subscribe)
        registry.enableSimpleBroker("/topic", "/queue");
        // Prefix for messages from clients to server
        registry.setApplicationDestinationPrefixes("/app");
        // Prefix for private messages to a specific user
        registry.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
            .setAllowedOrigins("http://localhost:3000")
            .withSockJS();  // Fallback for browsers without WebSocket
    }
}

消息控制器

@Controller
@RequiredArgsConstructor
@Slf4j
public class ChatController {

    private final SimpMessagingTemplate messagingTemplate;

    // Receives message and broadcasts to all subscribers of /topic/chat
    @MessageMapping("/chat.send")
    @SendTo("/topic/chat")
    public ChatMessage sendMessage(ChatMessage message, Principal principal) {
        message.setSender(principal.getName());
        message.setTimestamp(Instant.now());
        return message;
    }

    // Direct reply to the sender
    @MessageMapping("/chat.echo")
    @SendToUser("/queue/reply")
    public ChatMessage echoMessage(ChatMessage message) {
        message.setContent("Echo: " + message.getContent());
        return message;
    }

    // Programmatic send to a specific user
    @MessageMapping("/chat.private")
    public void sendPrivateMessage(PrivateMessage message, Principal principal) {
        message.setSender(principal.getName());
        messagingTemplate.convertAndSendToUser(
            message.getRecipient(),
            "/queue/private",
            message
        );
    }

    // Broadcast to all
    public void broadcastNotification(NotificationMessage notification) {
        messagingTemplate.convertAndSend("/topic/notifications", notification);
    }
}
// DTOs
public record ChatMessage(
    String id, String sender, String content,
    Instant timestamp, MessageType type
) {}

public enum MessageType { CHAT, JOIN, LEAVE, TYPING }

public record PrivateMessage(
    String sender, String recipient, String content, Instant timestamp
) {}

事件处理器

@Component
@RequiredArgsConstructor
@Slf4j
public class WebSocketEventListener {

    private final SimpMessagingTemplate messagingTemplate;
    private final OnlineUserService onlineUserService;

    @EventListener
    public void handleSessionConnected(SessionConnectedEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        String sessionId = accessor.getSessionId();
        Principal principal = accessor.getUser();

        if (principal != null) {
            String username = principal.getName();
            onlineUserService.userConnected(username, sessionId);
            messagingTemplate.convertAndSend("/topic/users.online",
                new UserStatusMessage(username, UserStatus.ONLINE));
        }
    }

    @EventListener
    public void handleSessionDisconnect(SessionDisconnectEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        String sessionId = accessor.getSessionId();

        onlineUserService.findBySessionId(sessionId).ifPresent(username -> {
            onlineUserService.userDisconnected(sessionId);
            messagingTemplate.convertAndSend("/topic/users.online",
                new UserStatusMessage(username, UserStatus.OFFLINE));
        });
    }
}

完整参考:有关完整的安全配置和验证,请参阅 security.md。


安全要点

@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {

    @Bean
    public AuthorizationManager<Message<?>> messageAuthorizationManager(
            MessageMatcherDelegatingAuthorizationManager.Builder messages) {
        return messages
            .nullDestMatcher().permitAll()
            .simpSubscribeDestMatchers("/topic/public/**").permitAll()
            .simpSubscribeDestMatchers("/topic/**", "/queue/**").authenticated()
            .simpDestMatchers("/app/**").authenticated()
            .anyMessage().authenticated()
            .build();
    }
}

完整参考:有关 JWT 身份验证、CSRF 防护和消息验证,请参阅 security.md。


会话属性和标头

@Controller
public class ChatController {

    @MessageMapping("/chat.join")
    @SendTo("/topic/chat")
    public ChatMessage joinChat(
            @Payload JoinRequest request,
            @Header("simpSessionId") String sessionId,
            SimpMessageHeaderAccessor headerAccessor) {

        // Save attributes in the WebSocket session
        headerAccessor.getSessionAttributes().put("username", request.username());
        headerAccessor.getSessionAttributes().put("roomId", request.roomId());

        return new ChatMessage(null, request.username(),
            request.username() + " joined!", Instant.now(), MessageType.JOIN);
    }
}

错误处理

@ControllerAdvice
public class WebSocketExceptionHandler {

    @MessageExceptionHandler
    @SendToUser("/queue/errors")
    public ErrorMessage handleException(Exception e) {
        return new ErrorMessage("ERROR", e.getMessage(), Instant.now());
    }

    @MessageExceptionHandler(AccessDeniedException.class)
    @SendToUser("/queue/errors")
    public ErrorMessage handleAccessDenied(AccessDeniedException e) {
        return new ErrorMessage("ACCESS_DENIED",
            "You don't have permission", Instant.now());
    }
}

public record ErrorMessage(String code, String message, Instant timestamp) {}

心跳配置

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/queue")
            .setHeartbeatValue(new long[]{10000, 10000})  // Server, Client in ms
            .setTaskScheduler(heartBeatScheduler());
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Bean
    public TaskScheduler heartBeatScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(1);
        scheduler.setThreadNamePrefix("ws-heartbeat-");
        scheduler.initialize();
        return scheduler;
    }

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
        registry.setMessageSizeLimit(128 * 1024)      // 128KB max message
            .setSendBufferSizeLimit(512 * 1024)       // 512KB send buffer
            .setSendTimeLimit(20 * 1000);             // 20s send timeout
    }
}

完整参考:有关 RabbitMQ 和 Redis 外部代理配置,请参阅 brokers.md。


最佳实践

应做不应做
使用 STOMP + SockJS 兼容不同浏览器仅使用原始 WebSocket
实现心跳以检测断开连接依赖 TCP keepalive
使用外部代理(RabbitMQ)进行扩展在生产环境使用简单代理
处理前验证负载信任客户端输入
妥善处理断开连接仅在内存中保存状态

不应使用此技能的情况

  • REST API - 使用 spring-rest 技能
  • 简单 SSE - 使用 SseEmitter 端点
  • NestJS WebSocket - 使用 nestjs-websocket 技能
  • React 客户端 - 前端使用 react-websocket 技能

反模式

反模式问题解决方案
连接被拒绝未配置端点验证 registerStompEndpoints
403 Forbidden未配置 CORS添加 setAllowedOrigins
没有会话Principal 为 null配置 WebSocket 身份验证
生产环境使用简单代理无法水平扩展使用 RabbitMQ/Redis 适配器
状态保存在内存中重启后丢失使用外部会话存储

快速故障排查

问题诊断修复
连接被拒绝检查端点配置验证 registerStompEndpoints
403 Forbidden检查 CORS添加 setAllowedOrigins
Principal 为 null检查身份验证配置实现 AuthChannelInterceptor
消息过大检查大小限制增大 setMessageSizeLimit
心跳超时检查间隔正确配置心跳

参考文件

文件内容
brokers.mdRabbitMQ、Redis 外部代理配置
security.mdJWT 身份验证、CSRF、消息验证
advanced.md底层 WebSocket、JS 客户端、测试

外部文档

评论

支持 Markdown