Backend Development 12 min read

Building a Real‑Time Chat with Spring Boot WebSocket and Java

This guide walks through setting up a Spring Boot 2.3.9 WebSocket server, defining message types and enums, implementing encoder/decoder, configuring the endpoint and bean, and creating a simple HTML/JavaScript client to test real‑time messaging between multiple users.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Building a Real‑Time Chat with Spring Boot WebSocket and Java

Environment

Spring Boot 2.3.9.RELEASE.

Dependencies

<code>&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
&lt;/dependency&gt;
&lt;dependency&gt;
  &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
  &lt;artifactId&gt;spring-boot-starter-websocket&lt;/artifactId&gt;
&lt;/dependency&gt;</code>

Message Types

Define an abstract message class and concrete subclasses for ping, system, and person‑to‑person messages, plus an enum that maps each type to a string code.

<code>public class AbstractMessage {
  protected String type;      // 消息类型
  protected String content;   // 消息内容
  protected String date;      // 消息日期
}

public class PingMessage extends AbstractMessage {
  public PingMessage() {}
  public PingMessage(String type) { this.type = type; }
}

public class SystemMessage extends AbstractMessage {
  public SystemMessage() {}
  public SystemMessage(String type, String content) {
    this.type = type;
    this.content = content;
  }
}

public class PersonMessage extends AbstractMessage {
  private String fromName;
  private String toName;
}

public enum MessageType {
  /** 系统消息 0000; 心跳检查消息 0001; 点对点消息 2001 */
  SYSTEM("0000"), PING("0001"), PERSON("2001");
  private String type;
  private MessageType(String type) { this.type = type; }
  public String getType() { return type; }
  public void setType(String type) { this.type = type; }
}</code>

WebSocket Server Endpoint

The GMessageListener class is annotated with @ServerEndpoint to handle client connections, message routing, logging, and error handling.

<code>@ServerEndpoint(value = "/message/{username}",
  encoders = {WsMessageEncoder.class},
  decoders = {WsMessageDecoder.class},
  subprotocols = {"gmsg"},
  configurator = MessageConfigurator.class)
@Component
public class GMessageListener {
    public static ConcurrentMap<String, UserSession> sessions = new ConcurrentHashMap<>();
    private static Logger logger = LoggerFactory.getLogger(GMessageListener.class);
    private String username;

    @OnOpen
    public void onOpen(Session session, EndpointConfig config, @PathParam("username") String username) {
        UserSession userSession = new UserSession(session.getId(), username, session);
        this.username = username;
        sessions.put(username, userSession);
        logger.info("【{}】用户进入, 当前连接数:{}", username, sessions.size());
    }

    @OnClose
    public void onClose(Session session, CloseReason reason) {
        UserSession userSession = sessions.remove(this.username);
        if (userSession != null) {
            logger.info("用户【{}】, 断开连接, 当前连接数:{}", username, sessions.size());
        }
    }

    @OnMessage
    public void pongMessage(Session session, PongMessage message) {
        ByteBuffer buffer = message.getApplicationData();
        logger.debug("接受到Pong帧【这是由浏览器发送】:" + buffer.toString());
    }

    @OnMessage
    public void onMessage(Session session, AbstractMessage message) {
        if (message instanceof PingMessage) {
            logger.debug("这里是ping消息");
            return;
        }
        if (message instanceof PersonMessage) {
            PersonMessage personMessage = (PersonMessage) message;
            if (this.username.equals(personMessage.getToName())) {
                logger.info("【{}】收到消息:{}", this.username, personMessage.getContent());
            } else {
                UserSession userSession = sessions.get(personMessage.getToName());
                if (userSession != null) {
                    try {
                        userSession.getSession().getAsyncRemote()
                            .sendText(new ObjectMapper().writeValueAsString(message));
                    } catch (JsonProcessingException e) {
                        e.printStackTrace();
                    }
                }
            }
            return;
        }
        if (message instanceof SystemMessage) {
            logger.info("接受到消息类型为【系统消息】");
            return;
        }
    }

    @OnError
    public void onError(Session session, Throwable error) {
        logger.error(error.getMessage());
    }
}</code>

Encoder and Decoder

WsMessageEncoder converts AbstractMessage objects to JSON strings, while WsMessageDecoder parses incoming JSON back to the appropriate subclass based on the type field.

<code>public class WsMessageEncoder implements Encoder.Text<AbstractMessage> {
    private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class);
    @Override public void init(EndpointConfig endpointConfig) {}
    @Override public void destroy() {}
    @Override public String encode(AbstractMessage tm) throws EncodeException {
        String message = null;
        try {
            message = new ObjectMapper().writeValueAsString(tm);
        } catch (JsonProcessingException e) {
            logger.error("JSON处理错误:{}", e);
        }
        return message;
    }
}

public class WsMessageDecoder implements Decoder.Text<AbstractMessage> {
    private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class);
    private static Set<String> msgTypes = new HashSet<>();
    static {
        msgTypes.add(MessageType.PING.getType());
        msgTypes.add(MessageType.SYSTEM.getType());
        msgTypes.add(MessageType.PERSON.getType());
    }
    @Override public AbstractMessage decode(String s) throws DecodeException {
        AbstractMessage message = null;
        try {
            ObjectMapper mapper = new ObjectMapper();
            Map<String, String> map = mapper.readValue(s, Map.class);
            String type = map.get("type");
            switch (type) {
                case "0000": message = mapper.readValue(s, SystemMessage.class); break;
                case "0001": message = mapper.readValue(s, PingMessage.class); break;
                case "2001": message = mapper.readValue(s, PersonMessage.class); break;
            }
        } catch (JsonProcessingException e) {
            logger.error("JSON处理错误:{}", e);
        }
        return message;
    }
    @Override public boolean willDecode(String s) {
        Map<String, String> map = new HashMap<>();
        try {
            map = new ObjectMapper().readValue(s, Map.class);
        } catch (JsonProcessingException e) { e.printStackTrace(); }
        logger.debug("检查消息:【" + s + "】是否可以解码");
        String type = map.get("type");
        if (StringUtils.isEmpty(type) || !msgTypes.contains(type)) {
            return false;
        }
        return true;
    }
    @Override public void init(EndpointConfig endpointConfig) {}
    @Override public void destroy() {}
}</code>

Configurator and Configuration

MessageConfigurator logs handshake request and response headers. WebSocketConfig registers a ServerEndpointExporter bean for jar deployment (not needed when deploying as a WAR on Tomcat).

<code>public class MessageConfigurator extends ServerEndpointConfig.Configurator {
    private static Logger logger = LoggerFactory.getLogger(MessageConfigurator.class);
    @Override public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        logger.debug("握手请求头信息:" + request.getHeaders());
        logger.debug("握手响应头信息:" + response.getHeaders());
        super.modifyHandshake(sec, request, response);
    }
}

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}</code>

Front‑End Page

A minimal HTML page loads g-messages.js , opens a WebSocket connection to ws://localhost:8080/message/{username} , displays incoming messages in a list, and sends messages typed by the user.

<code>&lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;script src="g-messages.js?v=1"&gt;&lt;/script&gt;
  &lt;title&gt;WebSocket&lt;/title&gt;
  &lt;script&gt;
    let gm = null;
    let username = null;
    function ListenerMsg({url, protocols = ['gmsg'], options = {}}) {
      if (!url) { throw new Error("未知服务地址"); }
      gm = new window.__GM({ url, protocols });
      gm.open(options);
    }
    ListenerMsg.init = (user) => {
      if (!user) { alert("未知的当前登录人"); return; }
      let url = `ws://localhost:8080/message/${user}`;
      let msg = document.querySelector("#msg");
      ListenerMsg({url, options: { onmessage(e) {
        let data = JSON.parse(e.data);
        let li = document.createElement("li");
        li.innerHTML = "【" + data.fromName + "】对你说:" + data.content;
        msg.appendChild(li);
      }}});
    };
    function enter() {
      username = document.querySelector("#nick").value;
      ListenerMsg.init(username);
      document.querySelector("#chat").style.display = "block";
      document.querySelector("#enter").style.display = "none";
      document.querySelector("#cu").innerText = username;
    }
    function send() {
      let toName = document.querySelector("#toname").value;
      let content = document.querySelector("#content").value;
      gm.sendMessage({type: "2001", content, fromName: username, toName});
      document.querySelector("#toname").value = '';
      document.querySelector("#content").value = '';
    }
  &lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div id="enter"&gt;
    &lt;input id="nick"/&gt;
    &lt;button type="button" onclick="enter()"&gt;进入&lt;/button&gt;
  &lt;/div&gt;
  &lt;hr/&gt;
  &lt;div id="chat" style="display:none;"&gt;
    当前用户:&lt;b id="cu"&gt;&lt;/b&gt;&lt;br/&gt;
    用户:&lt;input id="toname" name="toname"/&gt;&lt;br/&gt;&lt;br/&gt;
    内容:&lt;textarea id="content" rows="3" cols="22"&gt;&lt;/textarea&gt;&lt;br/&gt;
    &lt;button type="button" onclick="send()"&gt;发送&lt;/button&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;ul id="msg"&gt;&lt;/ul&gt;
  &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code>

Testing

Open two browser tabs, enter different usernames, and send messages. The screenshots below show the login screen, the chat interface, and successful message exchange.

Successful real‑time communication demonstrates a simple yet production‑ready WebSocket solution capable of handling tens of thousands of concurrent users.

Javareal-time messagingbackend-developmentSpring Bootwebsocket
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.