Websocket 数据实时更新(消息提醒功能)异步+事件发布
需求
实现一个消息提醒功能,类似优快云中我的消息功能,这里是能实时接收到官方或其他用户给你发消息。
方法
我这里采用的是 Websocket +异步+事件发布,有些人是直接 Websocket +异步,具体区别放在后面
1、创建Websocket服务端(照抄即可,具体有备注),前端通过调用/ws/message接口就可以获取连接。
MessageRealTime
是我的消息实体,
配置
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverendpointExporter(){
return new ServerEndpointExporter();
}
}
创建服务端
/**
* @Description WebSocket 服务端,监听地址为 /ws/message
**/
@ServerEndpoint("/ws/message")
@Component
@Slf4j
public class WsServerEndPoint {
// 使用线程安全的ConcurrentHashMap管理用户Session
private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
/**
* 客户端连接建立时触发
*/
@OnOpen
public void onOpen(Session session) {
String userId = AuthContext.getUserId();
try {
// 如果已有连接,关闭旧连接
Session oldSession = sessionMap.get(userId);
if (oldSession != null && oldSession.isOpen()) {
oldSession.close();
log.info("关闭用户 {} 的旧连接", userId);
}
sessionMap.put(userId, session);
log.info("WebSocket 连接建立:userId={}", userId);
} catch (Exception e) {
log.error("WebSocket onOpen 异常,userId={}", userId, e);
}
}
/**
* 接收客户端消息时触发
*/
@OnMessage
public void onMessage(String text, @PathParam("userId") String userId) {
log.info("收到用户 {} 的消息:{}", userId, text);
}
/**
* 客户端连接关闭时触发
*/
@OnClose
public void onClose(Session session, @PathParam("userId") String userId) {
sessionMap.remove(userId);
log.info("WebSocket 连接关闭:userId={}", userId);
}
/**
* WebSocket 发生错误时触发
*/
@OnError
public void onError(Session session, Throwable error, @PathParam("userId") String userId) {
log.error("WebSocket 出现错误,userId={}", userId, error);
}
/**
* 向指定用户发送消息
*/
public void sendMsgToUser(List<MessageRealTime> messageRealTimes) throws IOException {
ObjectMapper mapper = new ObjectMapper();
for (MessageRealTime messageRealTime : messageRealTimes) {
String toUserId = messageRealTime.getToUser();
Session session = sessionMap.get(toUserId);
if (session != null && session.isOpen()) {
String msgStr = mapper.writeValueAsString(messageRealTime);
session.getBasicRemote().sendText(msgStr);
log.info("向用户 {} 发送消息:{}", toUserId, msgStr);
} else {
log.warn("用户 {} 的连接不存在或已关闭,无法发送消息", toUserId);
}
}
}
MessageRealTime(消息实体)
@Data
@TableName("t_message_real_time")
@Schema(description = "实时消息实体类")
public class MessageRealTime extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "ID",type = IdType.ASSIGN_ID)
@Schema(description = "主键ID")
private String id;
@TableField("TITLE")
@Schema(description = "标题")
private String title;
@TableField("SEND_USER")
@Schema(description = "发送方ID")
private String sendUser;
@TableField("TO_USER")
@Schema(description = "接收方ID")
private String toUser;
@TableField("TYPE")
@Schema(description = "消息类型")
private String type;
@TableField("CONTENT")
@Schema(description = "内容")
private String content;
@TableField("SEND_TIME")
@Schema(description = "发送时间")
private LocalDateTime sendTime;
@TableField("SKIP_URL")
@Schema(description = "跳转URL")
private String skipUrl;
@TableField("IS_READ")
@Schema(description = "是否已读(0未读,1已读)")
private String isRead;
}
2、事件发布监听
/**
* 自定义的应用事件类,继承自 Spring 的 ApplicationEvent。
* 用于在应用中发布“消息推送”相关事件,携带需要推送的消息数据(messageList),
* 结合 Spring 的事件监听机制(@EventListener 和 @Async),可实现业务逻辑与消息推送逻辑的解耦,
* 同时实现异步消息发送,提升系统响应速度和扩展能力。
*/
public class MessageSendEvent extends ApplicationEvent {
private final List<MessageRealTime> messageList;
public MessageSendEvent(Object source, List<MessageRealTime> messageList) {
super(source);
this.messageList = messageList;
}
public List<MessageRealTime> getMessageList() {
return messageList;
}
}
事件监听,监听后异步推送消息
@Slf4j
@Component
@RequiredArgsConstructor
public class MessageSendListener {
private final WsServerEndPoint wsServerEndPoint;
@Async
@EventListener
public void handleMessageSendEvent(MessageSendEvent event) {
try {
wsServerEndPoint.sendMsgToUser(event.getMessageList());
} catch (Exception e) {
log.error("消息推送失败", e);
}
}
}
3、发送消息接口(随便写在哪都行只要你能调用,我这里写在了service实现类中)
@Service
public class MessageRealTimeServiceImpl extends ServiceImpl<MessageRealTimeMapper, MessageRealTime> implements MessageRealTimeService {
@Resource
private ApplicationEventPublisher eventPublisher;
@Override
public void sendMsg(List<MessageRealTime> messageRealTimes) {
// 1.保存数据
this.saveBatch(messageRealTimes);
// 2.发布事件
eventPublisher.publishEvent(messageRealTimes);
}
}
4、使用(直接调我们的发送消息方法接口)
messageRealTimeService.sendMsg(messageRealTimes);
5、前端对接:(这个我随便AI的不知道对不对,连接倒是没问题,其他的大家酌情参考)
- 建立 WebSocket 连接
const wsUrl = `ws://your-domain.com/myWs/${userId}?token=${token}`;
const socket = new WebSocket(wsUrl);
2.监听连接事件
socket.onopen = () => {
console.log('WebSocket连接成功');
};
socket.onerror = (err) => {
console.error('WebSocket连接出错', err);
};
socket.onclose = () => {
console.warn('WebSocket连接已关闭');
};
- 接收服务端推送的消息
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('收到消息', data);
// 示例处理逻辑:弹出提醒
if (data.title && data.content) {
// 比如使用 Element Plus 的 ElNotification
ElNotification({
title: data.title,
message: data.content,
type: 'info',
duration: 5000
});
}
// 可根据 data.type 做不同业务跳转
};
前端接收到消息可以是重新调接口刷新局部数据或在原来数据的基础上加数据,具体做法取决于业务。
总结
到这就结束了,估计有点懵,我大致表述一下整个过程:
一、前端建立连接
当前端调用 /ws/message 接口成功建立 WebSocket 连接后,服务端即可在某些业务条件下实时推送消息给前端。
例如,在你的审核业务中,A 审核完成后需要 B 审核,那么在 A 审核完成的逻辑中,就需要调用:
messageRealTimeService.sendMsg(messageRealTimes);
来触发发送消息提醒。
二、消息发送的两大步骤
-
保存业务数据
将构建好的消息实体 messageRealTimes 正常保存至数据库中。 -
消息推送
通过事件发布机制触发异步推送,实现消息的实时通知。
三、为什么不用简单的 @Async 异步注解?
很多时候,我们习惯直接在 sendMsg 方法上添加 @Async 来实现异步:
@Async
public void sendMsg(List<MessageRealTime> messageList) {
this.saveBatch(messageList);
wsServerEndPoint.sendMsgToUser(messageList);
}
这种做法虽然可以异步执行,但存在严重缺陷:
❌ 事务失效:保存数据和推送消息都在异步线程中,事务无法生效,可能导致数据还没提交就推送消息。
❌ 数据未提交,前端查不到:前端收到通知,但访问接口时查不到数据库记录。
❌ 保存失败无法回滚:推送失败时无法让调用方感知,也无法回滚业务操作。
❌ 扩展性差:将业务保存与推送耦合,不易复用、维护和扩展。
四、推荐方案:事件发布 + 异步监听
使用 Spring 的事件机制,主线程中只负责保存业务数据和发布事件,由监听器异步处理消息推送逻辑。
五、后续扩展
后续需要添加短信通知、写日志,也可以很方便地新增监听器方法或监听器类,只需要在 MessageSendEvent
类中添加信的消息类型即可,加上handleMessageSendEvent
对应的处理推送方法即可。比如:
public class MessageSendEvent extends ApplicationEvent {
private final List<MessageRealTime> messageList;
private final List<NoticeInfo> noticeInfos;
public MessageSendEvent(Object source, List<MessageRealTime> messageList, List<NoticeInfo> noticeInfos) {
super(source);
this.messageList = messageList;
this.noticeInfos = noticeInfos;
}
public List<MessageRealTime> getMessageList() {
return messageList;
}
public List<NoticeInfo> getNoticeInfos() {
return noticeInfos;
}
}
@Slf4j
@Component
@RequiredArgsConstructor
public class MessageSendListener {
private final WsServerEndPoint wsServerEndPoint;
@Async
@EventListener
public void handleMessageSendEvent(MessageSendEvent event) {
try {
wsServerEndPoint.sendMsgToUser(event.getMessageList());
} catch (Exception e) {
log.error("消息推送失败", e);
}
}
@Async
@EventListener
public void handleNoticeSendEvent(MessageSendEvent event) {
try {
wsServerEndPoint.sendMsgToUser(event.getNoticeInfos()); //推送方法需要重新写,改变一下接收类型即可。
} catch (Exception e) {
log.error("消息推送失败", e);
}
}
}