Spring BootでWebSocketを使ったリアルタイム通信を実装する方法 - STOMPとSockJSの基本


本記事は Spring Boot 3.x(Java 17以上) を対象としています。

HTTP APIのREST開発には慣れてきたけど、チャットや通知のようなリアルタイム機能をどう実装すればいいか迷ったことはないですか?そんなときに使うのが WebSocket です。

Spring Bootでは STOMPSockJS を組み合わせることで、シンプルな設定でリアルタイム通信機能を実装できます。この記事では、ブロードキャスト型チャット機能をゼロから作る手順をステップバイステップで解説します。

WebSocket・STOMP・SockJSの関係を整理する

まず3つの技術の役割をざっくり整理しましょう。

  • WebSocket は双方向通信を可能にするトランスポート層のプロトコルです。HTTPのように「リクエストしてレスポンスを待つ」のではなく、サーバーとクライアントがいつでもメッセージを送り合えます。
  • STOMP (Simple Text Oriented Messaging Protocol)はWebSocketの上に乗るメッセージングプロトコルで、publish/subscribeモデルを提供します。「このトピックを購読する」「このトピックにメッセージを送る」という形で通信を整理できます。
  • SockJS はWebSocketをサポートしていないブラウザやネットワーク環境向けのフォールバックライブラリです。WebSocketが使えない場合に自動でHTTPベースの代替手段に切り替えてくれます。

Spring Bootは spring-boot-starter-websocket 一つでこのスタック全体をサポートしています。

依存関係の追加

Mavenの場合は pom.xml に以下を追加します。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Gradleの場合はこちらです。

implementation 'org.springframework.boot:spring-boot-starter-websocket'

WebSocket設定クラスの実装

設定の核心となるクラスです。@EnableWebSocketMessageBroker を有効にして、エンドポイントとブローカーを設定します。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // クライアントが接続するエンドポイント。withSockJS() でフォールバックを有効化
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // /app で始まるメッセージはアプリケーションの @MessageMapping に転送
        registry.setApplicationDestinationPrefixes("/app");
        // /topic で始まるメッセージはインメモリブローカーが全購読者に配信
        registry.enableSimpleBroker("/topic");
    }
}

/app はクライアントからサーバーへの送信先プレフィックス、/topic はブロードキャスト配信先のプレフィックスです。この2つの役割の違いを押さえておくと、後の流れがスムーズになります。

メッセージ受信ハンドラの実装

クライアントからのメッセージを受け取るコントローラを作ります。DTOも合わせて定義しておきましょう。

public class ChatMessage {
    private String sender;
    private String content;

    public String getSender() { return sender; }
    public void setSender(String sender) { this.sender = sender; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
}

@Controller
public class ChatController {

    @MessageMapping("/chat")       // /app/chat 宛てのメッセージを受信
    @SendTo("/topic/messages")     // /topic/messages の購読者全員に返信
    public ChatMessage handleMessage(ChatMessage message) {
        return message;
    }
}

@MessageMapping はHTTPの @RequestMapping と同じ感覚で使えます。/app プレフィックスはSpringが自動で解釈するので、ここでは /chat と書くだけでOKです。

サーバーサイドからのブロードキャスト(SimpMessagingTemplate)

コントローラの外から能動的にメッセージをプッシュしたいときは SimpMessagingTemplate を使います。定期通知やイベント発生時のプッシュなどに便利です。

@Service
public class NotificationService {

    private final SimpMessagingTemplate messagingTemplate;

    public NotificationService(SimpMessagingTemplate messagingTemplate) {
        this.messagingTemplate = messagingTemplate;
    }

    public void sendNotification(String message) {
        messagingTemplate.convertAndSend("/topic/notifications", message);
    }
}

convertAndSend の第一引数がトピックのパス、第二引数がペイロードです。@Scheduled のスケジュールタスクや別のServiceクラスからも呼び出せます。

JavaScriptクライアントの実装

フロントエンド側は SockJS@stomp/stompjs(v5以降) を使います。以下のHTMLをそのままブラウザで開いて動作確認できます。

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Chat</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs/bundles/stomp.umd.min.js"></script>
</head>
<body>
    <input id="message" placeholder="メッセージを入力">
    <button onclick="sendMessage()">送信</button>
    <div id="output"></div>

    <script>
        const client = new StompJs.Client({
            webSocketFactory: () => new SockJS('/ws')
        });

        client.onConnect = () => {
            client.subscribe('/topic/messages', (msg) => {
                const body = JSON.parse(msg.body);
                document.getElementById('output').innerHTML +=
                    `<p>${body.sender}: ${body.content}</p>`;
            });
        };

        client.activate();

        function sendMessage() {
            const content = document.getElementById('message').value;
            client.publish({
                destination: '/app/chat',
                body: JSON.stringify({ sender: 'user1', content: content })
            });
        }
    </script>
</body>
</html>

旧来の Stomp.over(socket) パターンは stompjs(v2.x)のAPIです。2015年以降メンテナンスが止まっており非推奨となっているため、現在は @stomp/stompjs が後継として推奨されています。

Spring SecurityとWebSocketの統合

Spring Securityが導入済みのプロジェクトでは、WebSocketエンドポイントが 403 でブロックされることがあります。以下の設定で解消できます。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/ws/**").permitAll()  // WebSocketエンドポイントを許可
                .anyRequest().authenticated()
            )
            .csrf(csrf -> csrf
                .ignoringRequestMatchers("/ws/**")      // WebSocketはCSRF保護の対象外に
            );
        return http.build();
    }
}

WebSocketはHTTPのCSRFトークンの仕組みと相性が悪いため、WebSocketエンドポイントに限って無効化するのが一般的な対処法です。認証済みユーザーのみ接続させたい場合は permitAll()authenticated() に変更すればOKです。

なお、この設定はHTTPハンドシェイクレベルの保護が対象です。「どのトピックを誰が購読・送信できるか」というWebSocketメッセージレベルのアクセス制御はスコープ外となります。JWTと組み合わせた認証設定の詳細については Spring BootにJWT認証を実装する方法 を参照してください。

動作確認の手順

アプリを起動したら、HTMLファイルをブラウザの複数タブで開いてみましょう。片方のタブで送信したメッセージがもう一方にも届くことで、ブロードキャストの動作を確認できます。

Chrome DevToolsの ネットワークタブ でWSフィルタを選ぶと、送受信されているSTOMPフレームを確認できます。接続時に CONNECT フレームが流れているのが見えるはずです。

うまく動かないときは以下を確認してみてください。

  • 403エラー はSpring SecurityがWebSocketエンドポイントをブロックしているケースがほとんどです。permitAll() の設定を見直しましょう。
  • 接続は成功するがメッセージが届かない 場合は、/app/topic プレフィックスの設定ミスが多いです。送信先と購読先のパスを再確認してください。
  • CORSエラー が出る場合は registerStompEndpoints.setAllowedOrigins("*") を追加できますが、これは開発・動作確認用途に限ってください。本番では許可オリジンを明示するか setAllowedOriginPatterns を検討してください。詳細は Spring BootのCORS設定ガイド を参照。

まとめ

Spring Boot + STOMP + SockJSでリアルタイム通信を実装する流れを見てきました。設定クラスでエンドポイントとブローカーを定義し、@MessageMapping でメッセージを受け取り、SimpMessagingTemplate でプッシュ通知を送る、この3層の構造が基本です。

WebSocketのリクエスト処理にインターセプターを使いたいシーンでは、FilterとInterceptorの違いと使い方 も参考にしてみてください。