Backend Development 11 min read

Build a Simple Spring Boot WebSocket Chat: Full Code Walkthrough

This guide demonstrates how to set up a Spring Boot 2.4.12 WebSocket server with custom message types, encoders, decoders, endpoint configuration, and a minimal HTML client, including complete Java code snippets and testing screenshots.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Build a Simple Spring Boot WebSocket Chat: Full Code Walkthrough

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

<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

<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 onMessage(Session session, AbstractMessage message) {
        if (message instanceof PingMessage) {
            logger.debug("这里是ping消息");
            return;
        }
        if (message instanceof PersonMessage) {
            PersonMessage pm = (PersonMessage) message;
            if (this.username.equals(pm.getToName())) {
                logger.info("【{}】收到消息:{}", this.username, pm.getContent());
            } else {
                UserSession target = sessions.get(pm.getToName());
                if (target != null) {
                    try {
                        target.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

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

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 {
        try {
            ObjectMapper mapper = new ObjectMapper();
            Map<String, String> map = mapper.readValue(s, Map.class);
            String type = map.get("type");
            switch (type) {
                case "0000": return mapper.readValue(s, SystemMessage.class);
                case "0001": return mapper.readValue(s, PingMessage.class);
                case "2001": return mapper.readValue(s, PersonMessage.class);
                default: return null;
            }
        } catch (JsonProcessingException e) {
            logger.error("JSON处理错误:{}", e);
            return null;
        }
    }
    @Override public boolean willDecode(String s) {
        try {
            Map<String, String> map = new ObjectMapper().readValue(s, Map.class);
            String type = map.get("type");
            return type != null && msgTypes.contains(type);
        } catch (JsonProcessingException e) {
            return false;
        }
    }
    @Override public void init(EndpointConfig config) {}
    @Override public void destroy() {}
}</code>

Configurator and Spring Configuration

<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>

Frontend Page

<code>&lt;!doctype html&gt;
&lt;html&gt;
<head>
  &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; }
      const url = `ws://localhost:8080/message/${user}`;
      const msg = document.querySelector("#msg");
      ListenerMsg({url, options: {onmessage(e) {
        const data = JSON.parse(e.data);
        const 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() {
      const toName = document.querySelector("#toname").value;
      const content = document.querySelector("#content").value;
      gm.sendMessage({type: "2001", content, fromName: username, toName});
      document.querySelector("#toname").value = '';
      document.querySelector("#content").value = '';
    }
  &lt;/script&gt;
</head>
<body>
  <div id="enter">
    <input id="nick"/><button type="button" onclick="enter()">进入</button>
  </div>
  <hr/>
  <div id="chat" style="display:none;">
    当前用户:<b id="cu"></b><br/>
    用户:<input id="toname" name="toname"/><br/><br/>
    内容:<textarea id="content" rows="3" cols="22"></textarea><br/>
    <button type="button" onclick="send()">发送</button>
  </div>
  <div><ul id="msg"></ul></div>
</body>
</html></code>

Testing

Open two browser tabs, log in with different usernames, and send messages to each other. The screenshots below show the login page, the chat interface, and successful message exchange.

The simple WebSocket implementation runs in production with 8 GB memory handling 60 000 concurrent users.

Javabackend developmentSpring BootWebSocketChat
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.