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.
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
<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><!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; }
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 = '';
}
</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, 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.
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.