アーキテクチャ概要と役割設計
Spring BootとSTOMP over WebSocketを組み合わせたリアルタイム通信システムを構築します。画面は「管理者用(送信権限)」と「一般閲覧用(購読のみ)」に分離し、サーバー側でメッセージのブロードキャストを担う構成を採用します。WebJarsによりフロントエンド依存ライブラリのバージョン管理を一元化し、STOMPプロトコルによる標準的なメッセージフレームングを実現します。
クライアント側実装(HTML/JavaScript)
役割に応じたUI構成を定義し、通信ロジックは共通モジュールで管理します。以下の例は管理者用画面の構造を示しています。閲覧用画面は送信エリアのHTMLブロックをコメントアウトまたは削除するだけで対応可能です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>リアルタイム通知ダッシュボード</title>
<link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<script src="/webjars/jquery/jquery.min.js"></script>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<script src="/js/notification-client.js"></script>
</head>
<body>
<div class="container mt-4" id="app-root">
<div class="row mb-3">
<div class="col-md-6">
<div class="btn-group" role="group">
<button type="button" id="btn-link" class="btn btn-primary">接続開始</button>
<button type="button" id="btn-cut" class="btn btn-secondary" disabled>切断</button>
</div>
</div>
<div class="col-md-6 admin-only">
<div class="input-group">
<input type="text" id="msg-input" class="form-control" placeholder="配信メッセージを入力">
<button type="button" id="btn-publish" class="btn btn-success">配信</button>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="table-responsive" id="feed-panel" style="display:none;">
<table class="table table-bordered">
<thead class="table-dark">
<tr><th>通知内容</th><th>受信時刻</th></tr>
</thead>
<tbody id="msg-list"></tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
コア通信モジュール(JavaScript)
STOMPクライアントのライフサイクル管理とデータレンダリングを関数化します。変数名と制御フローを最適化し、接続状態に応じたUI切り替えとXSS対策を標準的に実装しています。
let wsSession = null;
const UI = {
btnLink: $('#btn-link'),
btnCut: $('#btn-cut'),
msgList: $('#msg-list'),
feedPanel: $('#feed-panel'),
toggleState(isConnected) {
this.btnLink.prop('disabled', isConnected);
this.btnCut.prop('disabled', !isConnected);
this.feedPanel.toggle(isConnected);
this.msgList.empty();
}
};
function establishConnection() {
const transport = new SockJS('/ws/hub');
wsSession = Stomp.over(transport);
wsSession.connect({}, (frame) => {
console.log('[STOMP] セッション確立:', frame);
UI.toggleState(true);
// トピック購読登録
wsSession.subscribe('/topic/system_alerts', (payload) => {
const data = JSON.parse(payload.body);
renderFeedEntry(data);
});
});
}
function terminateConnection() {
if (wsSession) {
wsSession.disconnect();
}
UI.toggleState(false);
console.log('[STOMP] セッション切断完了');
}
function broadcastMessage() {
const inputVal = $('#msg-input').val().trim();
if (!inputVal) return;
wsSession.send('/app/notify', {}, JSON.stringify({
text: inputVal,
timestamp: Date.now()
}));
$('#msg-input').val('');
}
function renderFeedEntry(data) {
const dateStr = new Date(data.timestamp).toLocaleString('ja-JP');
const row = `<tr>
<td class="align-middle">${escapeHtml(data.text)}</td>
<td class="align-middle text-muted">${dateStr}</td>
</tr>`;
UI.msgList.prepend(row);
}
function escapeHtml(unsafe) {
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
$(document).ready(() => {
$('#btn-link').on('click', establishConnection);
$('#btn-cut').on('click', terminateConnection);
$('#btn-publish').on('click', broadcastMessage);
});
サーバー側実装(Spring Boot)
クライアントから到着したSTOMPフレームをインターセプトし、指定した宛先URLへメッセージを再送します。SpringのWebSocketサポートにより、スレッドセーフなブロードキャスト配信が保証されます。
package com.example.websocket.controller;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import com.example.websocket.model.RequestPayload;
import com.example.websocket.model.ResponsePayload;
@Controller
public class NotificationController {
/**
* クライアント送信エンドポイントとブロードキャスト宛先のマッピング
*/
@MessageMapping("/notify")
@SendTo("/topic/system_alerts")
public ResponsePayload handleBroadcast(RequestPayload payload) {
return new ResponsePayload(payload.getText(), System.currentTimeMillis());
}
}
// モデル定義例(Java 14+ record推奨)
// public record RequestPayload(String text) {}
// public record ResponsePayload(String text, long timestamp) {}
データフローと動作検証
- 接続フェーズ:クライアントは
/ws/hub経由でSockJSトランスポートを確立し、STOMPオーバーレイを初期化します。 - 購読フェーズ:
connectコールバック内で/topic/system_alertsへのサブスクライブフレームをサーバーへ送信。これ以降、サーバーからのプッシュ配信を待ち受け状態に入ります。 - 送信フェーズ:管理者画面で配信ボタンを押下すると、
/app/notify宛てにJSONペイロードを格納したSTOMPフレームが送信されます。 - ブロードキャストフェーズ:Spring Controllerがメッセージを処理し、
@SendToで指定されたトピックへ再送。これにより、サブスクライブ中の全クライアントのコールバック関数が同期的にトリガーされます。 - レンダリングフェーズ:クライアント側で受信データをDOM操作し、UIへ反映。HTMLエスケープ処理を施すことでセキュリティリスクを排除します。