You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现可保留消息历史记录的Spring WebSocket?

Spring WebSocket 历史消息留存实现方案

问题场景

我正在构建一款需要留存WebSocket已发送历史消息的Spring WebSocket,期望实现的成功场景如下:

Browser 1:

CHAT
TestUser: Hi
TestUser2: Bye 

Browser 2(在两条消息都发送完成后才启动):

CHAT
TestUser: Hi
TestUser2: Bye
// 以上消息在连接建立时自动加载,因为WebSocket已留存这些消息

但目前我的实现效果是:

Browser 1:

CHAT
TestUser: Hi
TestUser2: Bye 

Browser 2(在两条消息都发送完成后才启动):

CHAT
// 无任何消息...

想知道该如何正确实现这个功能?


实现步骤

1. 新增全局消息存储组件

先定义一个单例的消息存储类,用来统一保存所有聊天历史消息:

@Component
public class ChatMessageHistory {
    // 用线程安全的LinkedList保证消息顺序,也可根据需求替换为其他集合
    private final List<String> messageHistory = Collections.synchronizedList(new LinkedList<>());

    // 添加消息到历史记录,可选限制消息数量避免内存溢出
    public void addMessage(String message) {
        messageHistory.add(message);
        if (messageHistory.size() > 100) {
            messageHistory.remove(0);
        }
    }

    // 获取所有历史消息的副本,避免外部修改原集合
    public List<String> getMessageHistory() {
        return new ArrayList<>(messageHistory);
    }
}

2. 发送消息时同步存储

在消息处理方法中,每发送一条实时消息,就同步保存到历史记录:

@Controller
public class ChatController {
    private final SimpMessagingTemplate messagingTemplate;
    private final ChatMessageHistory messageHistory;

    // 构造注入依赖
    public ChatController(SimpMessagingTemplate messagingTemplate, ChatMessageHistory messageHistory) {
        this.messagingTemplate = messagingTemplate;
        this.messageHistory = messageHistory;
    }

    @MessageMapping("/chat/send")
    public void sendMessage(ChatMessage message) {
        String formattedMsg = message.getUsername() + ": " + message.getContent();
        // 先存历史再广播
        messageHistory.addMessage(formattedMsg);
        messagingTemplate.convertAndSend("/topic/chat", formattedMsg);
    }
}

3. 新客户端连接时推送历史消息

监听WebSocket连接事件,当新客户端建立连接后,主动推送历史消息给该客户端:

@Component
public class WebSocketConnectionListener implements ApplicationListener<SessionConnectedEvent> {
    private final SimpMessagingTemplate messagingTemplate;
    private final ChatMessageHistory messageHistory;

    public WebSocketConnectionListener(SimpMessagingTemplate messagingTemplate, ChatMessageHistory messageHistory) {
        this.messagingTemplate = messagingTemplate;
        this.messageHistory = messageHistory;
    }

    @Override
    public void onApplicationEvent(SessionConnectedEvent event) {
        // 获取新连接的会话ID
        String sessionId = event.getMessage().getHeaders().get(SimpMessageHeaderAccessor.SESSION_ID_HEADER, String.class);
        
        // 构造消息头,指定推送目标会话
        SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
        headerAccessor.setSessionId(sessionId);
        headerAccessor.setLeaveMutable(true);

        // 逐条推送历史消息给新客户端
        List<String> history = messageHistory.getMessageHistory();
        for (String msg : history) {
            messagingTemplate.convertAndSendToUser(sessionId, "/queue/chat-history", msg, headerAccessor.getMessageHeaders());
        }
    }
}

4. 前端接收并渲染历史消息

前端STOMP客户端需要订阅专属的历史消息通道,在连接成功后接收并渲染历史消息:

const stompClient = Stomp.over(new SockJS('/ws'));

stompClient.connect({}, function(frame) {
    // 订阅历史消息通道
    stompClient.subscribe('/user/queue/chat-history', function(message) {
        addMessageToChat(message.body);
    });
    // 订阅实时消息通道
    stompClient.subscribe('/topic/chat', function(message) {
        addMessageToChat(message.body);
    });
});

// 渲染消息到聊天窗口的工具方法
function addMessageToChat(content) {
    const chatDiv = document.getElementById('chat');
    chatDiv.innerHTML += content + '<br>';
}

关键注意事项

  • 内存存储的历史消息会在应用重启后丢失,如需持久化,可将ChatMessageHistory的存储逻辑替换为操作Redis或MySQL。
  • 必须保证存储集合的线程安全,避免多线程环境下的数据异常。
  • 若历史消息量较大,建议实现分页加载逻辑,避免一次性推送大量消息导致客户端卡顿。

内容的提问来源于stack exchange,提问作者AwesomeDude091

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 19:05:23