3
8

More than 1 year has passed since last update.

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

Posted at

はじめに

普段はフロントエンドな私ですが、ちょっとバックエンドも触りたくなり、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に格納し、getUserssetUserするためのストレージです。

フロント側

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

まとめ

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

3
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
8