はじめに
普段はフロントエンドな私ですが、ちょっとバックエンドも触りたくなり、JavaのWebアプリケーション向けフレームワークであるSpring Bootに入門しました。
勉強進める中で、ちょうど良さそうなチュートリアル動画があったので成果物のアプリの解説をしてみたいと思います。
チュートリアルの動画:Spring Boot WebSocket: Chat Example
コード:github:spring-websocket-chat
依存関係
spring-boot-starter-websocketが必要です。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
ディレクトリ構成
ディレクトリ構成は以下のような形です。
MVCに本来は外部のDBやストレージである、storageディレクトリとwebsocketのconfigurationが追加されているという感じです。
バックエンド
src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── hogehoge
│ │ │ ├── WebSocketProjectApplication.java
│ │ │ ├── configuration
│ │ │ │ └── WebsocketConfiguration.java
│ │ │ ├── controller
│ │ │ │ ├── MessageController.java
│ │ │ │ └── UsersController.java
│ │ │ ├── model
│ │ │ │ └── MessageModel.java
│ │ │ └── storage
│ │ │ └── UserStorage.java
フロントエンド
frontend
│ ├── css
│ │ └── style.css
│ ├── index.html
│ ├── js
│ │ ├── chat.js
│ │ ├── custom.js
コード説明
※packageやimportは省略
configuration
WebsocketConfiguration.java
概要:プロジェクト全体でWebSocketを有効化するための設定を行うためのクラス
@Configuration
@EnableWebSocketMessageBroker
public class WebsocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app").enableSimpleBroker("/topic");
}
}
registerStompEndpoints
でSTOMP(Simple Text Oriented Messaging Protocol/単純で軽量なメッセージングプロトコル)を使ったWebSocketのやりとりを開始するエンドポイントを設定します。
ここでは、/chat
をエンドポイントに設定し、(本来良くないですが)ワイルドカードで全てのオリジンからのクロスオリジンリクエストを許可しています。
(最新のSpring-bootだとsetAllowedOriginPattern
にしないと動かない?)
最後のwithSockJS
はクライアント側でSockJS
を利用することの宣言です。
configureMessageBroker
は宛先ごとの処理を定義しています。
宛先が/app
の場合、Controllerに渡って処理され、その処理結果をメッセージブローカーに送り、/topic
の場合は直接メッセージブローカーに送られます。
controller
MessageController.java
概要:クライアント側で送信されるメッセージを処理するクラス
@RestController
public class MessageController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
@MessageMapping("/chat/{to}")
public void sendMessage(@DestinationVariable String to, MessageModel message) {
System.out.println("handling send message: " + message + " to: " + to);
boolean isExists = UserStorage.getInstance().getUsers().contains(to);
if (isExists) {
simpMessagingTemplate.convertAndSend("/topic/messages/" + to, message);
}
}
}
クライアント側で/app/chat/{to}
に向けて送信されたとき、宛先のユーザーが存在しているかどうかを確認し、
存在していればそのユーザーに向けてメッセージを送ります。
UsersController.java
概要:ユーザーの登録、更新処理を行うクラス
@RestController
@CrossOrigin
public class UsersController {
@GetMapping("/registration/{userName}")
public ResponseEntity<Void> register(@PathVariable String userName) {
System.out.println("handling register user request: " + userName);
try {
UserStorage.getInstance().setUser(userName);
} catch (Exception e) {
return ResponseEntity.badRequest().build();
}
return ResponseEntity.ok().build();
}
@GetMapping("/fetchAllUsers")
public Set<String> fetchAll() {
return UserStorage.getInstance().getUsers();
}
}
クライアント側で/registration/{userName}
へのGETリクエストが来た時、UserStorage
(後述)にユーザーを登録します。
/fetchAllUsers
へのGETリクエストが来た時は、UserStorage
からユーザー一覧を取得します。
model
MessageModel.java
概要:メッセージとログインユーザー(自分)をgetしたりsetしたりするクラス
public class MessageModel {
private String message;
private String fromLogin;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getFromLogin() {
return fromLogin;
}
public void setFromLogin(String fromLogin) {
this.fromLogin = fromLogin;
}
@Override
public String toString() {
return "MessageModel{" +
"message='" + message + '\'' +
", fromLogin='" + fromLogin + '\'' +
'}';
}
}
特に説明することはないです。
storage
UserStorage.java
概要:ユーザーネームを保存するだけの簡易的なストレージ
public class UserStorage {
private static UserStorage instance;
private Set<String> users;
private UserStorage() {
users = new HashSet<>();
}
public static synchronized UserStorage getInstance() {
if (instance == null) {
instance = new UserStorage();
}
return instance;
}
public Set<String> getUsers() {
return users;
}
public void setUser(String userName) throws Exception {
if (users.contains(userName)) {
throw new Exception("User aready exists with userName: " + userName);
}
users.add(userName);
}
}
一意のユーザーをSet
に格納し、getUsers
やsetUser
するためのストレージです。
フロント側
説明しようと思いましたが、実質バックエンドにリクエストを送っているだけなので特に書くことがありませんでした。
コードを読んでくれ!
まとめ
チュートリアルやっただけだと、勝手にできた気になりがちですが、こうやって文章に起こすとアプリケーションのロジックが理解できて良いですね!
この記事が、「英語の動画だから...」と有用なチュートリアルを敬遠している人の助けになれば良いかなと思います。