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
<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>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.
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; }
}WebSocket Server Endpoint
The GMessageListener class is annotated with @ServerEndpoint to handle client connections, message routing, logging, and error handling.
@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());
}
}Encoder and Decoder
WsMessageEncoderconverts AbstractMessage objects to JSON strings, while WsMessageDecoder parses incoming JSON back to the appropriate subclass based on the type field.
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() {}
}Configurator and Configuration
MessageConfiguratorlogs handshake request and response headers. WebSocketConfig registers a ServerEndpointExporter bean for jar deployment (not needed when deploying as a WAR on Tomcat).
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();
}
}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.
<!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>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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
