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.
Environment
Spring Boot 2.3.9.RELEASE.
Dependencies
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency></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><!doctype html>
<html>
<head>
<meta charset="UTF-8">
<script src="g-messages.js?v=1"></script>
<title>WebSocket</title>
<script>
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 = '';
}
</script>
</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, 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.