系统工作台待办实时提醒,取代五分钟刷新一次,判断有没有新的待办,利用 WebSocket 实现

张开发
2026/4/19 11:07:02 15 分钟阅读

分享文章

系统工作台待办实时提醒,取代五分钟刷新一次,判断有没有新的待办,利用 WebSocket 实现
系统工作台待办实时提醒取代五分钟刷新一次判断有没有新的待办利用 WebSocket 实现一、实现步骤二、前端代码三、后端代码四、效果展示五、AI 啰嗦直接忽略你好作为一名 Java 程序员将传统的“轮询”Polling模式升级为基于 WebSocket 的“实时推送”Push模式是提升用户体验和系统性能的最佳实践。轮询不仅浪费带宽和服务器资源还会导致消息延迟最长可达5分钟。而 WebSocket 建立的是全双工通信服务器一旦有新待办可以毫秒级主动推送到前端。下面我将基于Spring Boot和Spring WebSocket提供一套完整的实现方案。️ 核心架构思路连接建立前端页面加载时建立与后端的 WebSocket 长连接。用户绑定连接建立后将WebSocket Session与当前的用户ID进行绑定通常使用MapUserId, Session或 Redis。事件触发当业务层Service产生新的待办事项时触发一个事件。消息推送后端监听该事件查找对应用户的 Session主动发送 JSON 消息。前端接收前端onmessage回调收到消息通过浏览器的通知 API 或 DOM 操作更新界面。第一步引入 Maven 依赖我们需要spring-boot-starter-websocket。dependencies!-- Web 基础依赖 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!-- WebSocket 依赖 --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-websocket/artifactId/dependency!-- Lombok (可选简化代码) --dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/optional/dependency/dependencies第二步配置 WebSocket创建一个配置类开启 WebSocket 支持并注册处理器。importorg.springframework.context.annotation.Configuration;importorg.springframework.web.socket.config.annotation.EnableWebSocket;importorg.springframework.web.socket.config.annotation.WebSocketConfigurer;importorg.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;ConfigurationEnableWebSocketpublicclassWebSocketConfigimplementsWebSocketConfigurer{privatefinalTodoWebSocketHandlertodoWebSocketHandler;// 注入自定义的 HandlerpublicWebSocketConfig(TodoWebSocketHandlertodoWebSocketHandler){this.todoWebSocketHandlertodoWebSocketHandler;}OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistryregistry){// 注册处理器的路径前端连接 ws://host:port/ws/todo// setAllowedOrigins(*) 允许跨域生产环境建议指定具体域名registry.addHandler(todoWebSocketHandler,/ws/todo).setAllowedOrigins(*);}}第三步实现 WebSocket Handler (核心逻辑)这是处理连接、断开和消息转发的核心类。我们需要维护一个Map来存储在线用户和他们的会话。注意在生产环境中如果服务是多实例部署集群不能只使用本地ConcurrentHashMap必须使用Redis Pub/Sub来广播消息。这里演示单机版逻辑。importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Component;importorg.springframework.web.socket.CloseStatus;importorg.springframework.web.socket.TextMessage;importorg.springframework.web.socket.WebSocketSession;importorg.springframework.web.socket.handler.TextWebSocketHandler;importjava.io.IOException;importjava.util.Map;importjava.util.concurrent.ConcurrentHashMap;Slf4jComponentpublicclassTodoWebSocketHandlerextendsTextWebSocketHandler{// 存储在线用户的 SessionKey 为 userIdValue 为 Session// 使用 ConcurrentHashMap 保证线程安全privatestaticfinalMapString,WebSocketSessionUSER_SESSION_MAPnewConcurrentHashMap();/** * 连接建立成功后 */OverridepublicvoidafterConnectionEstablished(WebSocketSessionsession)throwsException{// 获取用户ID通常从 URL 参数或 HTTP 握手头中获取// 例如ws://localhost:8080/ws/todo?userId1001StringuserId(String)session.getAttributes().get(userId);if(userId!null){USER_SESSION_MAP.put(userId,session);log.info(用户 [{}] 建立了 WebSocket 连接当前在线人数: {},userId,USER_SESSION_MAP.size());}}/** * 接收到客户端消息通常客户端不需要发什么主要是心跳检测 */OverrideprotectedvoidhandleTextMessage(WebSocketSessionsession,TextMessagemessage)throwsException{log.info(收到客户端消息: {},message.getPayload());// 可以处理心跳包}/** * 连接断开后 */OverridepublicvoidafterConnectionClosed(WebSocketSessionsession,CloseStatusstatus)throwsException{StringuserId(String)session.getAttributes().get(userId);if(userId!null){USER_SESSION_MAP.remove(userId);log.info(用户 [{}] 断开了连接当前在线人数: {},userId,USER_SESSION_MAP.size());}}/** * 处理异常 */OverridepublicvoidhandleTransportError(WebSocketSessionsession,Throwableexception)throwsException{log.error(WebSocket 传输异常,exception);}/** * 业务层调用此方法向指定用户推送消息 */publicvoidsendMessageToUser(StringuserId,Stringmessage){WebSocketSessionsessionUSER_SESSION_MAP.get(userId);if(session!nullsession.isOpen()){try{session.sendMessage(newTextMessage(message));log.info(成功推送消息给用户 [{}]: {},userId,message);}catch(IOExceptione){log.error(发送消息失败,e);}}else{log.warn(用户 [{}] 不在线消息推送失败,userId);}}}第四步业务触发与拦截器我们需要在连接建立前把userId放入 Session 属性中。这通常通过HandshakeInterceptor实现或者在 Controller 中处理。这里简单演示如何在业务层触发。假设你有一个TodoService当新增待办时importcom.fasterxml.jackson.databind.ObjectMapper;importorg.springframework.stereotype.Service;importjava.util.HashMap;importjava.util.Map;ServicepublicclassTodoService{privatefinalTodoWebSocketHandlerwebSocketHandler;privatefinalObjectMapperobjectMappernewObjectMapper();publicTodoService(TodoWebSocketHandlerwebSocketHandler){this.webSocketHandlerwebSocketHandler;}/** * 模拟新增待办事项的业务方法 */publicvoidcreateTodo(StringassigneeUserId,StringtodoTitle){// 1. 保存待办到数据库...System.out.println(保存待办: todoTitle);// 2. 构造推送消息MapString,ObjectmsgnewHashMap();msg.put(type,NEW_TODO);msg.put(title,todoTitle);msg.put(time,System.currentTimeMillis());try{// 3. 调用 WebSocket Handler 推送StringjsonMsgobjectMapper.writeValueAsString(msg);webSocketHandler.sendMessageToUser(assigneeUserId,jsonMsg);}catch(Exceptione){e.printStackTrace();}}}第五步前端实现 (HTML JavaScript)前端不再需要setInterval而是监听 WebSocket 消息。!DOCTYPEhtmlhtmllangzhheadmetacharsetUTF-8title待办实时提醒/title/headbodyh1我的工作台/h1dividstatus连接状态未连接/divulidtodo-list!-- 待办列表 --/ulscript// 假设当前登录用户 ID 为 1001constuserId1001;// 建立 WebSocket 连接constsocketnewWebSocket(ws://localhost:8080/ws/todo?userId${userId});// 连接打开socket.onopenfunction(){document.getElementById(status).innerText连接状态实时连接中...;document.getElementById(status).style.colorgreen;// 请求浏览器通知权限if(Notification.permission!granted){Notification.requestPermission();}};// 接收消息socket.onmessagefunction(event){constdataJSON.parse(event.data);if(data.typeNEW_TODO){// 1. 更新页面 DOMconstlistdocument.getElementById(todo-list);constlidocument.createElement(li);li.innerText[新]${data.title};list.prepend(li);// 2. 浏览器弹窗提醒 (可选)if(Notification.permissiongranted){newNotification(新待办通知,{body:你有新的待办任务${data.title},icon:/path/to/your/icon.png});}}};// 连接关闭socket.onclosefunction(){document.getElementById(status).innerText连接状态已断开5秒后重试...;document.getElementById(status).style.colorred;// 简单的断线重连机制setTimeout((){window.location.reload();},5000);};// 连接错误socket.onerrorfunction(err){console.error(WebSocket 错误,err);};/script/body/html 生产环境的关键点在实际的企业级开发中你还需要考虑以下几点集群问题Redis Pub/Sub如果后端部署了多台服务器例如 Nginx 负载均衡用户的连接可能连在 Server A但业务请求打到了 Server B。解决方案Server B 收到业务请求后不能直接查本地 Map而是将消息发布到Redis Channel。所有 Server 订阅该 ChannelServer A 收到 Redis 消息后发现是发给自己的用户再通过本地 Session 推送。心跳检测网络波动可能导致连接“假死”。前端需要每隔 30-60 秒发送一个ping消息后端收到后回复pong否则后端主动断开连接释放资源。安全性握手认证不要在 URL 参数里明文传userId。应该在建立 WebSocket 连接前通过 HTTP Header 传递 TokenJWT在HandshakeInterceptor中解析 Token 验证用户身份。消息可靠性WebSocket 是“尽力而为”的协议。如果用户断网了消息就丢了。对于极其重要的待办建议配合数据库的“未读状态”字段用户上线后通过 HTTP 接口拉取一次未读列表作为兜底。通过这套方案你的系统将彻底告别“5分钟刷新”实现真正的实时响应。

更多文章