spring-websocket について
Spring でリアルタイムメッセージ機能を構築する開発者向けの WebSocket スキルです。STOMP メッセージブローカー、コントローラー、イベント処理、セキュリティ設定を扱い、ハートビートや外部ブローカーの参考情報も含みます。
主な機能
- 01STOMP ブローカーの設定
- 02メッセージコントローラーとイベント処理
- 03WebSocket セキュリティ設定
- 04セッション属性とエラー処理
- 05ハートビートと外部ブローカーの参考情報
活用シーン
- 01Spring アプリでのチャット機能構築
- 02クライアント接続とメッセージイベントの処理
- 03WebSocket のセキュリティとブローカー設定の検討
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-websocketインストール後、Claude Code で/spring-websocketを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
advanced.mdbrokers.mdsecurity.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 Forbidden | CORS が設定されていない | setAllowedOrigins を追加する |
| セッションなし | Principal が null | WebSocket 認証を設定する |
| 本番環境でシンプルブローカーを使用 | 水平スケーリングができない | RabbitMQ/Redis アダプターを使用する |
| メモリ内の状態 | 再起動時に失われる | 外部セッションストアを使用する |
トラブルシューティングのクイックガイド
| 問題 | 診断 | 修正 |
|---|---|---|
| 接続拒否 | エンドポイント設定を確認する | registerStompEndpoints を確認する |
| 403 Forbidden | CORS を確認する | setAllowedOrigins を追加する |
| Principal が null | 認証設定を確認する | AuthChannelInterceptor を実装する |
| メッセージが大きすぎる | サイズ制限を確認する | setMessageSizeLimit を増やす |
| ハートビートのタイムアウト | 間隔を確認する | ハートビートを適切に設定する |
リファレンスファイル
| ファイル | 内容 |
|---|---|
| brokers.md | RabbitMQ、Redis の外部ブローカー設定 |
| security.md | JWT 認証、CSRF、メッセージ検証 |
| advanced.md | 低レベル WebSocket、JS クライアント、テスト |
外部ドキュメント
コメント
Markdown 対応