Spring Boot + WebSocketのチャットアプリのチュートリアルをやってみたので解説してみる

## はじめに
普段はフロントエンドな私ですが、ちょっとバックエンドも触りたくなり、JavaのWebアプリケーション向けフレームワークであるSpring Bootに入門しました。
勉強進める中で、ちょうど良さそうなチュートリアル動画があったので成果物のアプリの解説をしてみたいと思います。

チュートリアルの動画：[Spring Boot WebSocket: Chat Example
](https://www.youtube.com/watch?v=-ao3pX-UhQc&t=3374s&ab_channel=JavaMaster)
コード：[github:spring-websocket-chat
](https://github.com/caligula95/spring-websocket-chat)

## 依存関係
spring-boot-starter-websocketが必要です。

```xml
<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を有効化するための設定を行うためのクラス

```java
@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
概要：クライアント側で送信されるメッセージを処理するクラス

```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
概要：ユーザーの登録、更新処理を行うクラス

```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したりするクラス

```java
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
概要：ユーザーネームを保存するだけの簡易的なストレージ

```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`するためのストレージです。

### フロント側

説明しようと思いましたが、実質バックエンドにリクエストを送っているだけなので特に書くことがありませんでした。
コードを読んでくれ！


## まとめ
チュートリアルやっただけだと、勝手にできた気になりがちですが、こうやって文章に起こすとアプリケーションのロジックが理解できて良いですね！
この記事が、「英語の動画だから...」と有用なチュートリアルを敬遠している人の助けになれば良いかなと思います。