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, "您有一条新线索待接收"));
    }
}

总结

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

相关推荐
苹果酱05679 分钟前
一文读懂SpringCLoud
java·开发语言·spring boot·后端·中间件
掐指一算乀缺钱29 分钟前
SpringBoot 数据库表结构文档生成
java·数据库·spring boot·后端·spring
飞翔的佩奇1 小时前
xxl-job适配sqlite本地数据库及mysql数据库。可根据配置指定使用哪种数据库。
数据库·spring boot·mysql·sqlite·xxl-job·任务调度
luoluoal3 小时前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
ChinaRainbowSea3 小时前
十三,Spring Boot 中注入 Servlet,Filter,Listener
java·spring boot·spring·servlet·web
2的n次方_3 小时前
掌握Spring Boot数据库集成:用JPA和Hibernate构建高效数据交互与版本控制
数据库·spring boot·hibernate
青灯文案13 小时前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
二十雨辰3 小时前
[苍穹外卖]-12Apache POI入门与实战
java·spring boot·mybatis
用生命在耍帅ㅤ6 小时前
java spring boot 动态添加 cron(表达式)任务、动态添加停止单个cron任务
java·开发语言·spring boot
程序员-珍7 小时前
SpringBoot v2.6.13 整合 swagger
java·spring boot·后端