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. 01Spring アプリでのチャット機能構築
  2. 02クライアント接続とメッセージイベントの処理
  3. 03WebSocket のセキュリティとブローカー設定の検討

インストール

コマンドをコピーしてターミナルで実行します。

$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 キープアライブに依存する
スケーリングには外部ブローカー(RabbitMQ)を使用する本番環境でシンプルブローカーを使用する
処理前にペイロードを検証するクライアントからの入力を信頼する
切断を適切に処理する状態をメモリ内のみに保持する

このスキルを使用すべきでない場合

  • REST API - spring-rest スキルを使用してください
  • シンプルな SSE - SseEmitter エンドポイントを使用してください
  • NestJS WebSocket - nestjs-websocket スキルを使用してください
  • React クライアント - フロントエンドには react-websocket スキルを使用してください

アンチパターン

アンチパターン問題解決策
接続拒否エンドポイントが設定されていないregisterStompEndpoints を確認する
403 ForbiddenCORS が設定されていないsetAllowedOrigins を追加する
セッションなしPrincipal が nullWebSocket 認証を設定する
本番環境でシンプルブローカーを使用水平スケーリングができないRabbitMQ/Redis アダプターを使用する
メモリ内の状態再起動時に失われる外部セッションストアを使用する

トラブルシューティングのクイックガイド

問題診断修正
接続拒否エンドポイント設定を確認するregisterStompEndpoints を確認する
403 ForbiddenCORS を確認するsetAllowedOrigins を追加する
Principal が null認証設定を確認するAuthChannelInterceptor を実装する
メッセージが大きすぎるサイズ制限を確認するsetMessageSizeLimit を増やす
ハートビートのタイムアウト間隔を確認するハートビートを適切に設定する

リファレンスファイル

ファイル内容
brokers.mdRabbitMQ、Redis の外部ブローカー設定
security.mdJWT 認証、CSRF、メッセージ検証
advanced.md低レベル WebSocket、JS クライアント、テスト

外部ドキュメント

コメント