SpringBoot 整合WebSocket+事件发布机制 实现实时消息推送

前言

最近在工作碰到一个需求,项目背景是一款线索分享系统,主要给公安系统使用。其中一个基本功能是,用户A在系统中提交了一条线索,这条线索根据一定的逻辑,判断需要推送给其它某些用户,例如这里举例,这条消息需要推送给用户B,此时B用户的系统页面中需要进行实时弹窗提示

这个推送弹窗提示的效果,就可以使用webSocket来实现,webSocket不像传统的http请求,只能客户端去主动请求服务端,它可以实现服务端与客户端之间互发消息,进行实时的沟通。比方说在线的聊天室,就是使用了webSocket实现。

这个功能实现的过程中,还希望这个推送的过程,是一个异步操作,保存完线索信息之后,直接返回前端页面保存成功,推送的过程异步操作,不要影响保存的线程。因为这个项目是一个Springboot单体项目,所以这里就借助于SpringBoot的事件发布机制来完成。

这个项目由于是给公安系统使用的,所以项目是直接部署到公安内网,通过ip访问,所以本文就针对这种方式来演示如何使用webSocket,至于比方说配置了nginx反向代理的场景,这里就不涉及了。

项目依赖

真实项目不便透露,这里做了一个demo项目,SpringBoot版本为2.6.13,pom文件如下:

xml 复制代码
<dependencies>
    <!--springsecurity-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <!--thymeleaf-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
    <!--web场景启动器-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!--websocket-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    
    <!--mysql驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.17</version>
    </dependency>

    <!--mybatis-plus依赖-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1</version>
    </dependency>
</dependencies>

准备工作

表设计

数据表就简单设计了两个,一个是用户表,一个是线索表。

用户表:

线索表:

SpringSecurity

项目的登录功能是引入了SpringSecurity来做的,这里为了简单,直接使用了默认的表单登录流程,配置类如下:

java 复制代码
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserServiceImpl userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
}

注入的UserService实例中,实现了UserDetailsService接口,代表这是SpringSecurity中的用户信息数据源。然后实现了loadUserByUsername方法,也就是说登录时,会调用这个方法来根据username获取用户信息。

项目还引入Mybatis-plus,所以这里UserService继承了ServiceImpl类

java 复制代码
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService, UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = getOne(new QueryWrapper<UserEntity>().eq("username", username));
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        return user;
    }
}

流程

整个流程这里就简单规划一下,有两个用户 user1、user2,user1登录系统后,保存一条线索,保存完成后。假定这条线索需要推送给user2,这里让user2的页面,弹出提醒。

实现功能

引入依赖

要使用WebSocket,首先需要引入依赖,上面的pom文件中已经有了

配置类

需要提供一个配置类,里边定义一个ServerEndpointExporter,注册到容器中,如下

java 复制代码
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

定义WebSocket Server

这里需要定义一个WebSocket Server,下面就简称server,这个server可以理解成是一个websocket请求访问的服务端。因为websocket的工作机制,是需要客户端 先发起一个请求,请求被服务端接收到,服务端与客户端建立一个长链接,然后双方接下来可以互发消息。

而且server也可以定义多个,比方说有线索保存的推送,有人员报警的推送等等,就好比是一个个的controller。

首先定义一个类,标注上@Component注解以及@ServerEndpoint注解,后者可以用来指定当前类是一个WebSocket Server的服务端,同时也可以设置它的请求地址:

java 复制代码
@Component
@ServerEndpoint("/webSocket/clue/{id}")
public class WebSocketServer {
}

整个类的具体配置如下:

  1. @ServerEndpoint设置了当前服务端的请求路径,为/webSocket/clue/{id},这里的{id},其实就是一个路径参数,每一个id就对应一个websocket连接,建议这里的id直接和用户对应,也就是说,每个用户对应一个连接。
  2. 类中设置了几个成员变量:onlineSessionClientCount表示当前连接数,onlineSessionClientMap是一个map集合,里面存放所有在线的用户连接,pendingMap也是一个map集合,这里是对用户不在线时,堆积消息的一个处理,这里只是针对用户量不大时的简单处理,并且数据也是保存到内存中的。如果要求数据不能丢失,或者用户量比较大。也可以存到数据库,或者引入redis。
  3. onOpen、onClose、onMessage、onError这四个方法,分别标注了对应的注解,代表当用户连接成功时的操作、关闭连接时的操作、收到消息时的操作、连接出错时的操作。
  4. 最下面还有两个自定义方法,sendToOne也就是对某个id发消息,也就是对某个用户发消息。getWebSocketUrl是提供了一个静态方法,当有地方想要使用这个server时,就调用这个方法,获取webSocket的连接地址,返回给前端,由前端发起webSocket请求。
java 复制代码
@Component
@ServerEndpoint("/webSocket/clue/{id}")
public class WebSocketServer {

    //在线数量
    private static AtomicInteger onlineSessionClientCount = new AtomicInteger(0);

    //在线客户端集合
    private static Map<String, Session> onlineSessionClientMap = new ConcurrentHashMap<>();

    //堆积消息集合
    private static Map<String, List<String>> pendingMap = new ConcurrentHashMap<>();

    /**
     * 连接创建成功
     *
     * @param id
     * @param session
     */
    @OnOpen
    public void onOpen(@PathParam("id") String id, Session session) {
        //保存到在线客户端集合
        onlineSessionClientMap.put(id, session);
        //记录在线客户端数
        onlineSessionClientCount.incrementAndGet();
        //如果当前用户存在堆积数据 进行推送
        if (pendingMap.containsKey(id)) {
            List<String> list = pendingMap.get(id);
            pendingMap.remove(id);
            list.forEach(message -> sendToOne(id, message));
        }
    }

    /**
     * 连接关闭回调
     *
     * @param id
     * @param session
     */
    @OnClose
    public void onClose(@PathParam("id") String id, Session session) {
        //从map集合中移除
        onlineSessionClientMap.remove(id);
        //在线数减1
        onlineSessionClientCount.decrementAndGet();
    }

    /**
     * 收到消息后的回调
     *
     * @param message
     * @param session
     */
    @OnMessage
    public void onMessage(String message, Session session) {
    }

    /**
     * 发生错误时的回调
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
    }

    /**
     * 向指定的id发送消息
     *
     * @param id
     * @param message
     */
    public void sendToOne(String id, String message) {
        Session session = onlineSessionClientMap.get(id);
        if (session == null) {
            //如果该id不在线,记录消息 等待用户访问时推送
            List<String> msgs = new ArrayList<>();
            if (pendingMap.containsKey(id)) {
                msgs = pendingMap.get(id);
            }
            msgs.add(message);
            return;
        }
        session.getAsyncRemote().sendText(message);
    }

    /**
     * 获取websocket地址
     *
     * @param request
     * @return
     */
    public static String getWebSocketUrl(HttpServletRequest request) {
        String ip = request.getLocalAddr();
        if (ip.contains(":")) {
            ip = "localhost";
        }
        int port = request.getLocalPort();
        return "ws://" + ip + ":" + port + "/webSocket/clue/" + UserUtil.getLoginUser().getId();
    }
}

前端发起webSocket请求

可以参考下面的代码,具体就是页面初始化时,获取到对应的url,然后在js中发起请求,等待接收消息。接收到消息之后,就做某些操作,比如弹窗或者显示消息等等。

controller

java 复制代码
@RequestMapping("/")
@Controller
public class IndexController {
    @GetMapping
    public String index(HttpServletRequest request, Model model) {
        model.addAttribute("webSocketUrl", WebSocketServer.getWebSocketUrl(request));
        return "index";
    }
}

index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<input type="hidden" id="webSocketUrl" th:value="${webSocketUrl}"/>
<div id="tip" style="border:1px solid #F00;width:30%;display: none"/>
<script type="text/javascript">
    window.onload = function () {
        //创建websocket连接
        let url = document.getElementById("webSocketUrl").value;
        let socket = new WebSocket(url);

        //打开事件
        socket.onopen = function () {
            console.log("webSocket正在连接");
        };
        //获得消息事件
        socket.onmessage = function (msg) {
            let tip = document.getElementById("tip");
            tip.textContent = msg.data;
            tip.style.display = "";
        };
        //关闭事件
        socket.onclose = function () {
            console.log("webSocket已关闭");
        };
        //发生了错误事件
        socket.onerror = function () {
            console.log("webSocket发生错误")
        }
    }
</script>
</body>
</html>

保存线索

接下来就剩在保存线索数据的时候,调用server来进行推送了。

这里为了不让推送的过程影响到保存线程,使用@Async+事件发布机制,完成一个异步操作。

开启异步支持

首先为了开启异步支持,需要在主启动类或者配置类上,标注@EnableAsync注解

定义事件类

java 复制代码
public class ClueEvent extends ApplicationEvent {
    private String message;

    public ClueEvent(Object source, String message) {
        super(source);
        this.message = message;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

定义监听器

java 复制代码
@Component
public class ClueEventListener {

    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 监听器处理消息
     *
     * @param event
     */
    @Async
    @EventListener
    public void onEventListener(ClueEvent event) {
        //这里的id就写死成user2的id
        webSocketServer.sendToOne("2", event.getMessage());
    }
}

保存时发布事件

java 复制代码
@Controller
@RequestMapping("/clue")
public class ClueController {

    @Autowired
    private ClueService clueService;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @PostMapping("/save")
    @ResponseBody
    public void save(ClueEntity clueEntity) {
        clueService.save(clueEntity);
        //发布推送事件
        applicationEventPublisher.publishEvent(new ClueEvent(this, "您有一条新线索待接收"));
    }
}

总结

完成上述操作之后,就能够实现在保存线索时,实时的推送消息到指定的用户了。

相关推荐
2301_7930868738 分钟前
SpringCloud 02 服务治理 Nacos
java·spring boot·spring cloud
MacroZheng2 小时前
还在用WebSocket实现即时通讯?试试MQTT吧,真香!
java·spring boot·后端
midsummer_woo2 小时前
基于springboot的IT技术交流和分享平台的设计与实现(源码+论文)
java·spring boot·后端
别惹CC4 小时前
Spring AI 进阶之路01:三步将 AI 整合进 Spring Boot
人工智能·spring boot·spring
柯南二号5 小时前
【Java后端】Spring Boot 集成 MyBatis-Plus 全攻略
java·spring boot·mybatis
javachen__6 小时前
SpringBoot整合P6Spy实现全链路SQL监控
spring boot·后端·sql
IT毕设实战小研12 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
一只爱撸猫的程序猿13 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
甄超锋13 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
武昌库里写JAVA16 小时前
JAVA面试汇总(四)JVM(一)
java·vue.js·spring boot·sql·学习