目录
[使用 @ServerEndpoint 创建 WebSocket 端点](#使用 @ServerEndpoint 创建 WebSocket 端点)
[WebSocket 生命周期方法](#WebSocket 生命周期方法)
[Session 管理(保存在线用户)](#Session 管理(保存在线用户))
本文目标:能在 Spring Boot 中编写一个简单的 WebSocket 服务端
依赖导入
在 Spring Boot 项目中导入 WebSocket 依赖
Spring Boot 自带了对 WebSocket 的官方支持,只需添加一行依赖即可。
1. Maven 依赖
在 pom.xml 中加入:
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
这个 starter 会自动引入 spring-websocket 和 spring-messaging,能使用两种实现方式:
Javax 标准注解版(@ServerEndpoint)
Spring 原生配置版(WebSocketHandler)
2. 可选依赖(若用 STOMP 或 SockJS)
暂时不用,但以后可加:
XML
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
3. 启动类要求
Spring Boot 启动类无需特别修改,只要正常启动即可。
例如:
java
@SpringBootApplication
public class WebSocketDemoApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketDemoApplication.class, args);
}
}
使用 @ServerEndpoint 创建 WebSocket 端点
这是最常见也最直观的写法,底层基于 Java EE 的 WebSocket 标准 API(JSR 356)
Spring Boot 会自动扫描带 @ServerEndpoint 注解的类并注册成 WebSocket 端点。
注册端点 = 服务器对外开放了一个「实时通信接口」,客户端能主动连上来,之后双方想发消息就发,不用反复建立连接。
没注册的类,服务器不知道它是一个WebSocket 通信接口,不会把客户端的 WebSocket 连接路由到这个类。
1. 启用 @ServerEndpoint 支持
因为 @ServerEndpoint 来自 Javax 包,需要在配置类中启用 WebSocket 支持。
在项目中新建配置类,例如:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
/**
* 用于扫描和注册所有带 @ServerEndpoint 的 Bean
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2. 创建一个简单的 WebSocket 服务端
在项目中新增一个类,例如:
java
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
@ServerEndpoint("/ws/chat")
@Component
public class ChatEndpoint {
@OnOpen
public void onOpen(Session session) {
System.out.println("🟢 新连接建立:" + session.getId());
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("📩 收到消息:" + message + "(来自 " + session.getId() + ")");
}
@OnClose
public void onClose(Session session) {
System.out.println("🔴 连接关闭:" + session.getId());
}
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("⚠️ 出现错误:" + session.getId());
throwable.printStackTrace();
}
}
3. 启动项目后访问
启动 Spring Boot 项目后,WebSocket 端点地址为:
bash
ws://localhost:8080/ws/chat
现在可以用浏览器控制台或 VSCode WebSocket 插件连接这个地址来测试通信。
使用 const ws = new WebSocket('ws://localhost:8080/ws/chat');在浏览器测试

WebSocket 生命周期方法
WebSocket 端点类中的 4 个核心注解方法(@OnOpen、@OnMessage、@OnClose、@OnError)
控制了从连接建立到断开的整个生命周期。
(1)@OnOpen 客户端连接时触发
java
@OnOpen
public void onOpen(Session session) {
System.out.println("🟢 新连接建立:" + session.getId());
}
触发时机:
浏览器或客户端调用 new WebSocket("ws://...") 连接成功后立即触发
常用场景:
向客户端发送欢迎消息
把 Session 对象加入到在线用户集合中(后面群发要用)
记录在线人数或用户状态
(2)@OnMessage 收到消息时触发
java
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("📩 收到消息:" + message + "(来自 " + session.getId() + ")");
}
触发时机:
当客户端通过 ws.send("xxx") 发送消息时触发。
常用场景:
打印或处理收到的消息
实现业务逻辑(如聊天转发、命令执行)
调用 session.getBasicRemote().sendText() 回复客户端
(3)@OnClose 连接关闭时触发
java
@OnClose
public void onClose(Session session) {
System.out.println("🔴 连接关闭:" + session.getId());
}
触发时机:
客户端主动调用 ws.close();
网络中断或异常断开
服务端主动关闭连接
常用场景:
从在线用户集合中移除该 Session
广播"某某离开聊天室"的系统通知
清理资源
(4)@OnError 通信出错时触发
java
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("⚠️ 出错:" + session.getId());
throwable.printStackTrace();
}
触发时机:
网络异常、编码异常、消息格式错误等情况
常用场景:
打印日志定位问题
通知管理员或客户端错误信息
5. 方法触发顺序(生命周期流程图)
java
客户端连接 → @OnOpen →
客户端发送消息 → @OnMessage →
客户端关闭连接 → @OnClose →
过程中出错 → @OnError
每个 Session 代表一个独立的客户端连接
Session 管理(保存在线用户)
在 WebSocket 中,每当一个客户端连接建立,框架都会为它创建一个 Session 对象。
我们可以利用它来:
-
识别不同的用户;
-
发送私聊(单发)或群发消息;
-
统计在线人数;
-
在断开时清理资源。
1. 为什么要管理 Session?
假设你写了一个聊天室:
小明、小红、小刚 都连上了服务器;
当小明发消息时,服务器需要广播给所有在线用户;
这时服务器必须知道有哪些连接(Session)存在。
所以要用一个线程安全的集合来管理所有在线 Session。
2. 使用 ConcurrentHashMap 保存 Session
在 ChatEndpoint 中添加一个静态集合:
java
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
import jakarta.websocket.Session;
@ServerEndpoint("/ws/chat")
@Component
public class ChatEndpoint {
// 保存所有在线用户
private static final Map<String, Session> ONLINE_USERS = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session) {
ONLINE_USERS.put(session.getId(), session);
System.out.println("🟢 新连接:" + session.getId());
broadcast("🔔 用户 " + session.getId() + " 加入聊天室!");
}
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("📩 来自 " + session.getId() + ":" + message);
broadcast("💬 用户 " + session.getId() + " 说:" + message);
}
@OnClose
public void onClose(Session session) {
ONLINE_USERS.remove(session.getId());
System.out.println("🔴 连接关闭:" + session.getId());
broadcast("❌ 用户 " + session.getId() + " 离开聊天室。");
}
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("⚠️ 出错:" + session.getId());
throwable.printStackTrace();
}
/** 群发消息 */
private void broadcast(String message) {
ONLINE_USERS.forEach((id, s) -> {
try {
s.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
| 代码部分 | 说明 |
|---|---|
| ConcurrentHashMap<String, Session> | 线程安全地存储在线连接 |
| ONLINE_USERS.put() / remove() | 用户上线 / 离线管理 |
| broadcast() | 向所有在线 Session 群发消息 |
| session.getBasicRemote().sendText() | 向某一客户端发送消息 |
3. 运行结果预期
启动 Spring Boot 项目;
打开两个浏览器页面;
分别在 Console 执行:
java
const ws = new WebSocket("ws://localhost:8080/ws/chat");
ws.onmessage = e => console.log(e.data);
ws.send("你好");
ws.onmessage = e => console.log(e.data);是后端发啥消息过来,前端就自动把消息打印到控制台,能看到别人发的内容。
会看到:


这说明着群发逻辑生效,所有客户端都能接收到消息。
群发与单发消息实现
1. 群发(广播消息)
前一节已经实现过基础群发函数 broadcast():
java
private void broadcast(String message) {
ONLINE_USERS.forEach((id, session) -> {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
});
}
| 参数 | 实际存储的内容 | 相当于 |
|---|---|---|
| id | session.getId() | 用户的唯一标识(类似微信号) |
| s | Session 对象 | 用户的连接通道(类似微信聊天窗口) |
可以把 (id, s) -> { ... } 理解为:
"对于每一个(id, s)这样的配对,执行以下操作"
用途
向所有在线用户推送系统消息(如"XX 进入聊天室")
转发聊天内容给所有连接的客户端
触发示例
java
@OnMessage
public void onMessage(String msg, Session session) {
broadcast("💬 用户 " + session.getId() + " 说:" + msg);
}
2. 单发(私聊消息)
若每个用户登录后都有自己的唯一标识(例如用户名),可在连接时把它放入 URL 参数:
前端连接:
java
const ws = new WebSocket("ws://localhost:8080/ws/chat?username=Tom");
后端解析并存储:
java
@OnOpen
public void onOpen(Session session) {
String username = session.getQueryString().split("=")[1];
USER_MAP.put(username, session);
broadcast("🔔 用户 " + username + " 加入聊天室");
}
实现私聊:
java
public void sendToUser(String username, String message) {
Session session = USER_MAP.get(username);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
调用示例:
java
sendToUser("Tom", "这是一条私聊消息");
3. 群发 + 单发混合场景
实际聊天系统中会根据消息内容决定:
若前端发送的 JSON 包含目标用户:执行 sendToUser();
否则默认广播。
例如:
java
@OnMessage
public void onMessage(String json, Session session) {
JSONObject msg = new JSONObject(json);
String toUser = msg.optString("to");
String content = msg.optString("content");
if (toUser.isEmpty()) {
broadcast("💬 公聊:" + content);
} else {
sendToUser(toUser, "📨 私聊:" + content);
}
}
JSONObject 就是把 JSON 字符串转换成后端能方便操作的键值对字典,比如前端发{"to":"Tom","content":"你好"},转成 JSONObject 后,能直接按to和content取对应值
optString:从这个字典里按指定的键(比如to/content)取字符串值,就算键不存在也不会报错,只会返回空字符串,比直接取值更安全
实战练习
1. 创建 WebSocket 服务端
步骤:
在 ChatEndpoint 中实现 连接建立(onOpen)、消息接收(onMessage)和 连接关闭(onClose)等方法。
使用 ConcurrentHashMap 存储在线用户并广播消息。
java
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
@ServerEndpoint("/ws/chat")
@Component
public class ChatEndpoint {
// 存储所有在线用户
private static final Map<String, Session> ONLINE_USERS = new ConcurrentHashMap<>();
// 连接建立时
@OnOpen
public void onOpen(Session session) {
String username = session.getQueryString() != null ? session.getQueryString().split("=")[1] : session.getId();
ONLINE_USERS.put(username, session); // 将用户 session 放入集合
System.out.println("🟢 用户 " + username + " 已加入聊天室");
broadcast("🔔 用户 " + username + " 加入聊天室");
}
// 收到消息时
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("📩 收到消息:" + message);
broadcast("💬 " + session.getId() + " 说:" + message); // 广播给所有人
}
// 连接关闭时
@OnClose
public void onClose(Session session) {
String username = getUsernameBySession(session);
ONLINE_USERS.remove(username); // 从集合中移除
System.out.println("🔴 用户 " + username + " 离开聊天室");
broadcast("❌ 用户 " + username + " 离开聊天室");
}
// 错误发生时
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("⚠️ 错误:" + throwable.getMessage());
throwable.printStackTrace();
}
// 群发消息
private void broadcast(String message) {
ONLINE_USERS.forEach((username, session) -> {
try {
session.getBasicRemote().sendText(message); // 向每个用户发送消息
} catch (Exception e) {
e.printStackTrace();
}
});
}
// 根据 Session 获取用户名
private String getUsernameBySession(Session session) {
return ONLINE_USERS.entrySet().stream()
.filter(entry -> entry.getValue().equals(session))
.map(Map.Entry::getKey)
.findFirst()
.orElse(session.getId());
}
}
getUsernameBySession 方法
从存储 "用户名 - 连接" 的对照表中,根据用户的连接(Session)反查对应的用户名;如果没找到,就用连接的唯一 ID 当用户名。
entrySet:把 "用户名 - 连接" 对照表(ONLINE_USERS)拆成一个个 "键值对"(比如 <Tom, 连接 1>、<Jack, 连接 2>),方便逐个检查
stream:开启流式处理,能像流水线一样一步步筛选、转换数据
filter:筛选 只留下 "连接和目标 Session 匹配" 的那个键值对
entry:就是 "键值对" 的别名(比如 < Tom, 连接 1 > 这个整体)
map:转换 把筛选出的键值对,换成它的 "键"(也就是用户名)
findFirst:取第一个匹配的结果(因为一个连接只对应一个用户名)
orElse:兜底 如果没找到匹配的用户名,就用 Session 的 ID 代替
2. 测试功能:群发与单发
启动 Spring Boot 项目后,可以打开多个浏览器窗口模拟不同用户的连接。
例如:
用户 1:连接 ws://localhost:8080/ws/chat?username=Tom;
用户 2:连接 ws://localhost:8080/ws/chat?username=Alice。
功能测试:
用户 1 发送消息:其他用户会收到群聊消息;
用户 1 关闭连接:会广播"某某离开聊天室"的消息。
3. 前端连接与发送消息
可以在浏览器控制台中执行以下代码来模拟前端连接和消息发送:
java
// 连接 WebSocket
const ws = new WebSocket("ws://localhost:8080/ws/chat?username=Tom");
// 监听消息
ws.onmessage = (event) => {
console.log("📩 收到消息:", event.data);
};
// 发送消息
ws.send("大家好,我是Tom!");
// 关闭连接
ws.close();
ws.onmessage = e => console.log(e.data);
3. 测试结果

