コード全文

package com.example.demo.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.model.Message;

@RestController
@RequestMapping("/chat")
public class ChatController {

    private static final Queue<Message> messages = new ConcurrentLinkedQueue<>();

    @GetMapping
    public List<Message> getMessages() {
        return new ArrayList<>(messages);
    }

    @PostMapping
    public void postMessage(@RequestParam("user") String user,
                            @RequestParam("message") String message) {
        messages.add(new Message(user, message));
    }
}

package com.example.demo.model;

public class Message {
    private String user;
    private String content;

    public Message() {}  // JSON変換用

    public Message(String user, String content) {
        this.user = user;
        this.content = content;
    }

    public String getUser() {
        return user;
    }

    public String getContent() {
        return content;
    }
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>LINE風複数人チャット</title>
  <style>
    body {
      font-family: sans-serif;
      background-color: #e5ddd5;
      margin: 0;
      padding: 0;
      display: flex;
      flex-direction: column;
      height: 100vh;
    }
    #chatBox {
      flex: 1;
      padding: 10px;
      overflow-y: auto;
      background-color: #f7f7f7;
    }
    .message {
      max-width: 70%;
      padding: 8px 12px;
      margin: 6px 0;
      border-radius: 15px;
      clear: both;
    }
    .sent {
      background-color: #dcf8c6;
      float: right;
      text-align: right;
    }
    .received {
      background-color: #fff;
      float: left;
      text-align: left;
    }
    #inputArea {
      display: flex;
      padding: 10px;
      background-color: #fff;
      border-top: 1px solid #ccc;
    }
    #message, #username {
      padding: 8px;
      font-size: 1em;
      border: 1px solid #ccc;
      border-radius: 20px;
      margin-right: 10px;
    }
    #message {
      flex: 1;
    }
    #sendMessage {
      padding: 8px 20px;
      border: none;
      background-color: #34b7f1;
      color: white;
      border-radius: 20px;
      font-weight: bold;
      cursor: pointer;
    }
    .username-label {
      font-size: 0.75em;
      color: #555;
      display: block;
    }
    .clearfix::after {
      content: "";
      display: table;
      clear: both;
    }
  </style>
</head>
<body>

<div id="chatBox"></div>

<div id="inputArea">
  <input type="text" id="username" placeholder="名前" />
  <input type="text" id="message" placeholder="メッセージ" />
  <button id="sendMessage">送信</button>
</div>

<script>
  document.addEventListener("DOMContentLoaded", function () {
    const chatBox = document.getElementById("chatBox");
    const username = document.getElementById("username");
    const messageInput = document.getElementById("message");
    const sendButton = document.getElementById("sendMessage");

    function renderMessages(messages) {
      chatBox.innerHTML = '';
      messages.forEach(msg => {
        const div = document.createElement('div');
        const nameLabel = document.createElement('span');
        nameLabel.className = "username-label";
        nameLabel.textContent = msg.user;

        div.className = "message clearfix " + 
          (msg.user === username.value.trim() ? "sent" : "received");
        div.appendChild(nameLabel);
        div.appendChild(document.createTextNode(msg.content));
        chatBox.appendChild(div);
      });
      chatBox.scrollTop = chatBox.scrollHeight;
    }

    sendButton.addEventListener("click", function () {
      const user = username.value.trim();
      const message = messageInput.value.trim();
      if (!user || !message) return;

      fetch("/chat", {
        method: "POST",
        headers: {
          "Content-Type": "application/x-www-form-urlencoded"
        },
        body: `user=${encodeURIComponent(user)}&message=${encodeURIComponent(message)}`
      }).then(() => {
        messageInput.value = "";
      });
    });

    setInterval(() => {
      fetch("/chat")
        .then(res => res.json())
        .then(data => renderMessages(data));
    }, 1500);
  });
</script>

</body>
</html>

「LINEのような複数人チャットをどうやって作るのか?」というテーマで、新人エンジニアの方向けに、できるだけわかりやすく丁寧に解説していきます。

たとえば、LINEで友達とやりとりしているとき、自分のメッセージは右、友達のメッセージは左に表示されますよね?あの表示はどうやって実現されているのか、不思議に思ったことはありませんか?

その仕組みを、自分でも実装できるようになってみましょう!


なぜ「誰が送ったか」を管理する必要があるの?

チャットアプリは、「誰が」「どんな内容のメッセージを」「いつ」送ったのか、という情報をきちんと管理する必要があります。

今回の実装では、「いつ」は省略して、「誰が」「なにを送ったか」に絞って説明します。

チャットを複数人対応にするために必要な変更点

項目目的
ユーザー名の送信誰がメッセージを送ったかを識別するため
メッセージ構造の変更単なる文字列ではなく、{ユーザー名: メッセージ}の形で扱う
表示側での分岐自分のメッセージは右、他人のメッセージは左にする
サーバーのデータ構造変更Queue → Queue(オブジェクト)

Messageクラス:メッセージの入れ物を作る

まずは、サーバー側で扱うメッセージの「型」を用意します。これは、Javaでいうところのクラスを作る作業です。

package com.example.demo.model;

public class Message {
    private String user;
    private String content;

    public Message() {} // JSONから変換するために必要

    public Message(String user, String content) {
        this.user = user;
        this.content = content;
    }

    public String getUser() {
        return user;
    }

    public String getContent() {
        return content;
    }
}

なぜクラスにするの?

「文字列」だけを使っていた状態では、誰の発言かが分からないからです。

たとえば:

こんにちは

だけでは、誰が送ったのか分かりませんが、

{ "user": "Yamada", "content": "こんにちは" }

のようにすると、表示側で「Yamadaさんの発言」として処理できます。


ChatControllerの変更:POSTとGETでやりとりする

次に、サーバーの制御部分を変更します。

package com.example.demo.controller;

import com.example.demo.model.Message;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;

@RestController
@RequestMapping("/chat")
public class ChatController {

    private static final Queue<Message> messages = new ConcurrentLinkedQueue<>();

    @GetMapping
    public List<Message> getMessages() {
        return new ArrayList<>(messages);
    }

    @PostMapping
    public void postMessage(@RequestParam("user") String user,
                            @RequestParam("message") String message) {
        messages.add(new Message(user, message));
    }
}

重要ポイント

  • Queue:順番を保持したままメッセージを保存
  • GET:今までのメッセージを全部取得
  • POST:新しいメッセージを追加

フロントエンド(HTML + JavaScript)の工夫

ここでは「誰のメッセージか」に応じて、表示位置を変える処理をしています。

div.className = "message clearfix " + 
  (msg.user === username.value.trim() ? "sent" : "received");

この部分がポイントで、

  • 自分の名前と一致すれば sent(右寄せ)
  • 一致しなければ received(左寄せ)

というクラスを付けています。

HTML + CSS によるスタイルの違い

.sent {
  background-color: #dcf8c6;
  float: right;
  text-align: right;
}
.received {
  background-color: #fff;
  float: left;
  text-align: left;
}

これで、まるでLINEのような見た目になります!


ちょっとだけ用語解説

用語意味
Queue先入れ先出し(FIFO)のデータ構造。メッセージの順番を守るために使う
@RestControllerSpring BootでREST APIを定義するクラスに付ける注釈
@PostMapping, @GetMappingHTTPリクエスト(送信・取得)を受け取るための指定
JSONデータ構造を表現するための形式。JavaScript Object Notation の略。

まとめと今後の学習の指針

今回の内容では、複数人チャットを実現するために以下のステップを体験しました:

  • ユーザー情報を送信・管理する仕組みを作る
  • メッセージをオブジェクトとして扱うよう変更する
  • 表示上で自分と他人の発言を分ける

次に学ぶべきこと

  • WebSocket を使ったリアルタイム通信(今は1.5秒ごとのポーリング)
  • ログイン機能をつけてユーザー名を自動取得できるようにする
  • 日付・時刻の表示でチャット履歴をより見やすくする

「チャット」はとても身近な機能なので、楽しく学びながら成長できる分野です。ぜひ、自分なりのアレンジを加えたオリジナルチャットアプリも作ってみてください!


最後までお読みいただきありがとうございます。