<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.3.x</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>5.3.x</version>
</dependency>
<!-- Jackson JSON 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<beans xmlns:websocket="https://www.springframework.org/schema/websocket" ...>
<bean id="websocketEndpoint" class="cn.example.websocket.WebSocketEndpoint"/>
<bean id="handshakeInterceptor" class="cn.example.websocket.HandshakeInterceptor"/>
<websocket:handlers>
<websocket:mapping path="/websocket" handler="websocketEndpoint"/>
<websocket:handshake-interceptors>
<ref bean="handshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>
</beans>
public class HandshakeInterceptor extends HttpSessionHandshakeInterceptor {
private static final Logger log = LoggerFactory.getLogger(HandshakeInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
// 從 HTTP Session 獲取當前登錄用戶
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
UserInfo currentUser = UserManager.getSessionUser(attrs.getRequest());
UserSessionVo sessionVo = new UserSessionVo();
String userKey = (currentUser != null) ? currentUser.getEmail() : "guest_" + System.currentTimeMillis();
sessionVo.setUserKey(userKey);
sessionVo.setConnectTime(new Date());
// 放入 attributes,後續可在 WebSocketSession.getAttributes() 取出
attributes.put("USER_SESSION", sessionVo);
log.info("WebSocket 握手準備: userKey={}", userKey);
return super.beforeHandshake(request, response, wsHandler, attributes);
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {
log.info("WebSocket 握手完成");
super.afterHandshake(request, response, wsHandler, ex);
}
}
public class UserSessionVo implements Serializable {
private String userKey; // 用戶唯一標識
private Date connectTime; // 連接建立時間
private Date lastPingTime; // 最後心跳時間
private Date lastPushTime; // 最後推送時間
private WebSocketSession session; // 關聯的 WebSocket 會話
// getter/setter 省略
}
@Component
public class WebSocketEndpoint extends TextWebSocketHandler {
private static final Logger log = LoggerFactory.getLogger(WebSocketEndpoint.class);
@Autowired private MessagePushService pushService;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
UserSessionVo vo = (UserSessionVo) session.getAttributes().get("USER_SESSION");
if (vo != null) {
vo.setSession(session);
// 存入本地緩存或 Redis
SessionRegistry.register(vo.getUserKey(), vo);
// 觸發業務邏輯:推送離線消息、同步狀態等
pushService.onUserConnected(vo.getUserKey());
}
log.info("連接建立: {}", session.getId());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 處理客戶端發來的文本消息(如心跳、確認收到)
String payload = message.getPayload();
log.debug("收到消息: {}", payload);
// 可選:回傳確認
session.sendMessage(new TextMessage("ACK: " + payload));
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
UserSessionVo vo = (UserSessionVo) session.getAttributes().get("USER_SESSION");
if (vo != null) {
SessionRegistry.unregister(vo.getUserKey());
pushService.onUserDisconnected(vo.getUserKey());
}
log.info("連接關閉: {}, 狀態: {}", session.getId(), status);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
log.error("傳輸錯誤: {}", session.getId(), exception);
// 可觸發重連邏輯或告警
}
}
public class SessionRegistry {
private static final ConcurrentHashMap<String, UserSessionVo> REGISTRY = new ConcurrentHashMap<>();
public static void register(String userKey, UserSessionVo vo) {
REGISTRY.put(userKey, vo);
}
public static UserSessionVo get(String userKey) {
return REGISTRY.get(userKey);
}
public static void unregister(String userKey) {
REGISTRY.remove(userKey);
}
public static Collection<UserSessionVo> allSessions() {
return REGISTRY.values();
}
}
集群環境需替換為 Redis + 消息總線(如 Spring Cloud Stream / RabbitMQ)實現跨節點消息路由。