SpringBoot整合 - WebSocket


今日内容

  • ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼

    ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨


学习目标

  • ⑴ ⑵ ⑶ ⑷ ⑸ ⑹ ⑺ ⑻ ⑼ ⑽

    ⑾ ⑿ ⒀ ⒁ ⒂ ⒃ ⒄ ⒅ ⒆ ⒇

    ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨


一、引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.7.0</version>
</dependency>

二、核心配置

@Configuration
public class WebSocketConfiguration {

    /**
     * 开启@ServerEndpoint注解声明, WebSocketEndpoint
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

三、创建WebSocket工具类

注意: 在@ServerEndpoint修饰的类中, 无法直接通过@Autowired注入service或者bean

原因及解决方法: 查看SpringBoot使用@ServerEndpoint无法@Autowired依赖注入问题解决

@Slf4j
@Component
@ServerEndpoint("/inquire/modelSession/{qccUserId}")
public class ModelSessionWebSocket {

    /**
     * 存储Session集合
     */
    private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();

    /**
     * 存储对象集合
     */
    private static ConcurrentHashMap<String, SysUser> userMap = new ConcurrentHashMap<>();

    /**
     * 连接建立时的操作
     *
     * @param session
     * @param qccUserId
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("qccUserId") String qccUserId) throws Exception {
        // 判断该用户是否已经连接
        if (sessionMap.get(qccUserId) != null) {
            log.error("用户已经登录! {}", qccUserId);
            return;
        }
        // 判断是否有该用户
        ISysUserService sysUserService = SpringUtils.getBean(ISysUserService.class);
        SysUser sysUser = sysUserService.lambdaQuery().eq(SysUser::getQccUserId, qccUserId).one();
        if (sysUser == null) {
            log.error("[ModelSessionWebSocket] 提醒您: 未获取到用户信息! {}", qccUserId);
            session.close();
            return;
        }
        // 设置Map
        setMap(session, sysUser);
        // 获取在线人数
        log.info("用户连接: {}, 名称为: {}, 当前在线人数: {}", qccUserId, sysUser.getNickName(), sessionMap.size());
    }

    /**
     * 设置Map集合
     *
     * @param session 会话
     * @param sysUser 用户
     */
    private void setMap(Session session, SysUser sysUser) {
        // 获取用户Id
        String qccUserId = sysUser.getQccUserId();
        // 存储会话到会话集合
        sessionMap.put(qccUserId, session);
        // 存储用户到用户集合
        userMap.put(qccUserId, sysUser);
    }

    /**
     * 连接关闭时操作
     *
     * @param session 会话
     */
    @OnClose
    public void onClose(Session session) {
        String qccUserId = getQccUserIdBySession(session);
        removeMap(session);
        log.info("用户下线: {}, 当前在线人数: {}", qccUserId, sessionMap.size());
    }

    /**
     * 当用户关闭会话时, 移除用户会话记录
     *
     * @param session 会话
     */
    private void removeMap(Session session) {
         String qccUserId = getQccUserIdBySession(session);
         if (StrUtil.isEmpty(qccUserId)){
             // 如果用户不存在, 则直接结束
             return;
         }
         sessionMap.remove(qccUserId);
         userMap.remove(qccUserId);
    }

    /**
     * 根据会话找到用户id
     *
     * @param session 会话
     * @return
     */
    private String getQccUserIdBySession(Session session) {
        for (String qccUserId : sessionMap.keySet()) {
            if (sessionMap.get(qccUserId).getId().equals(session.getId())){
                return qccUserId;
            }
        }
        return null;
    }

    /**
     * 连接异常时操作
     *
     * @param throwable
     */
    @OnError
    public void onError(Throwable throwable) {
        log.error("连接异常: {}", throwable.getMessage());
        throwable.printStackTrace();
    }

    /**
     * 接收到消息时的操作
     *
     * @param session
     * @param message
     */
    @OnMessage
    public void onMessage(Session session, String message) {
        // 根据会话获取当前用户
        SysUser sysUser = getUserBySession(session);
        if (ObjectUtil.isNull(sysUser)) {
            log.info("未获取到用户信息!");
            return;
        }
        // 解析消息
        log.info("用户: {}, 接收到了消息: {}", sysUser.getQccUserId(), message);
        if (!JSONUtil.isTypeJSON(message)) {
            log.info("消息体异常: {}", message);
            return;
        }
        ModelSessionDto modelSessionDto = JSONUtil.toBean(message, ModelSessionDto.class);
        Session toSession = sessionMap.get(sysUser.getQccUserId());
        if (toSession == null){
            log.info("用户: {}不在线!", sysUser.getQccUserId());
            return;
        }
        // 发送消息
        // ModelSessionService modelSessionService = SpringUtils.getBean(ModelSessionService.class);
        // modelSessionService.conversation(modelSessionDto, session, sysUser);
        toSession.getBasicRemote().sendText("收到! 收到! 收到!");
    }

    /**
     * 根据会话找到用户
     *
     * @param session 会话
     * @return
     */
    private SysUser getUserBySession(Session session) {
        String qccUserId = getQccUserIdBySession(session);
        if (StrUtil.isEmpty(qccUserId)) {
            return null;
        }
        return userMap.get(qccUserId);
    }

}

四、以上即可使用, 具体的业务需要自行修改




@ServerEndpoint无法@Autowired依赖注入问题解决

1.问题

websocket里面使用@Autowired注入service或bean时,报空指针异常。

@Component
@ServerEndpoint("/groupChat/{groupNo}/{uuid}")
public class GroupChatWebsocket {

    @Autowired
    private RedisTemplateUtil redisTemplateUtil;
}

2.解决方案

2.1 使用static静态对象

private static RedisTemplateUtil redisUtil;
 
@Autowired
public static void setRedisTemplateUtil(RedisTemplateUtil redisUtil) {
	WebSocketServer.redisUtil = redisUtil;
}

2.2 动态的从spring容器中取出RedisTemplateUtil 推荐方案

package cn.pconline.framework.util;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * 获取spring容器
 * 当一个类实现了这个接口ApplicationContextAware之后,这个类就可以方便获得ApplicationContext中的所有bean。
 * 换句话说,这个类可以直接获取spring配置文件中所有有引用到的bean对象
 * 前提条件需作为一个普通的bean在spring的配置文件中进行注册 
 */
public class SpringCtxUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    	SpringCtxUtils.applicationContext = applicationContext;
    }


    public static <T> T getBean(Class<T> type) {
        try {
            return applicationContext.getBean(type);
        } catch (NoUniqueBeanDefinitionException e) {   // 出现多个,选第一个
            String beanName = applicationContext.getBeanNamesForType(type)[0];
            return applicationContext.getBean(beanName, type);
        }
    }

    public static <T> T getBean(String beanName, Class<T> type) {
        return applicationContext.getBean(beanName, type);
    }
}

<think>好的,用户想在Spring Boot 2.3中集成WebSocket,我得先理清楚需要的步骤。首先,记得Spring BootWebSocket有支持,可能需要添加相关的依赖。查看引用[1]里提到spring-websocket模块,所以应该在项目中引入这个依赖。不过Spring Boot通常通过starter来简化配置,所以可能需要找对应的starter。 接下来,配置WebSocket。可能需要一个配置类,使用@EnableWebSocketMessageBroker注解,然后继承AbstractWebSocketMessageBrokerConfigurer或者WebSocketMessageBrokerConfigurer(取决于Spring版本)。在Spring Boot 2.3中,可能用的是后者。然后覆盖registerStompEndpoints方法,添加端点,比如/ws,并启用SockJS支持。同时配置消息代理,可能使用简单的内存代理,或者集成RabbitMQ、ActiveMQ等。这里用户可能只需要基础的,所以用内存代理,设置应用前缀和代理目的地前缀。 然后,创建控制器处理消息。使用@MessageMapping注解来定义处理消息的方法,比如发送到/app/chat的消息会被对应的处理方法处理,然后通过@SendTo注解将结果广播到主题,比如/topic/messages。这样前端订阅这个主题就能收到消息了。 前端部分需要用到STOMP客户端,比如使用SockJS和stomp.js。需要建立连接,订阅主题,并发送消息到指定的端点。这部分需要编写JavaScript代码,连接到/ws端点,然后在连接成功后订阅/topic/messages,并处理收到的消息。 测试的话,可以创建一个简单的HTML页面,包含发送消息的表单和显示区域,通过JavaScript与后端交互。同时,确保跨域问题已经处理,或者在开发环境下配置允许跨域。 另外,用户可能想知道如何扩展,比如使用外部消息代理,这时候需要修改配置类,替换掉简单的内存代理,指向外部的RabbitMQ或ActiveMQ。不过用户的问题主要是集成教程,所以基础步骤可能足够。 检查引用中是否有其他相关信息。引用[3]提到Reactor框架,但WebSocket集成可能不需要直接使用Reactor,除非需要响应式支持。不过Spring Boot 2.3可能默认使用响应式的一些库,但这里用户可能只需要基本的WebSocket功能,所以暂时不考虑响应式部分。 综上,步骤应该是:添加依赖、配置WebSocket、创建消息处理控制器、编写前端代码。然后测试验证功能是否正常。可能需要提醒用户注意版本兼容性,确保Spring Boot 2.3对应的Spring版本兼容WebSocket配置。同时,在代码示例中正确使用注解和配置方法,比如用@Configuration注解配置类,覆盖正确的方法。</think>### 如何在Spring Boot 2.3中集成WebSocket? #### 1. 添加依赖 首先在 `pom.xml` 中添加 WebSocket 相关依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` Spring WebSocket 模块基于 STOMP 协议实现,依赖会自动引入 `spring-websocket` 和 `spring-messaging`[^1]。 --- #### 2. 配置 WebSocket 创建配置类 `WebSocketConfig`,启用 STOMP 协议支持: ```java @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 注册 WebSocket 端点,允许 SockJS 备用选项 registry.addEndpoint("/ws").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置应用消息前缀(客户端发送消息的目标前缀) registry.setApplicationDestinationPrefixes("/app"); // 启用简单内存消息代理(生产环境建议使用 RabbitMQ 或 Kafka) registry.enableSimpleBroker("/topic"); } } ``` - `/ws` 是客户端连接的端点地址 - `/topic` 用于广播消息,`/app` 用于点对点消息路由 --- #### 3. 创建消息控制器 定义控制器处理 WebSocket 消息: ```java @Controller public class MessageController { @MessageMapping("/chat") @SendTo("/topic/messages") public String handleMessage(String message) { return "Received: " + message; } } ``` - `@MessageMapping` 定义接收消息的地址(完整路径为 `/app/chat`) - `@SendTo` 指定广播消息的目标主题[^1] --- #### 4. 前端实现 HTML 页面使用 STOMP 客户端库(需引入 `sockjs.min.js` 和 `stomp.min.js`): ```html <script> const socket = new SockJS('/ws'); const stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { stompClient.subscribe('/topic/messages', function(response) { console.log('Received: ' + response.body); }); }); function sendMessage() { const message = document.getElementById('message').value; stompClient.send("/app/chat", {}, message); } </script> ``` --- #### 5. 测试验证 1. 启动 Spring Boot 应用 2. 访问 HTML 页面,通过浏览器控制台观察消息收发 3. 多开页面测试广播功能 --- #### 扩展:集成外部消息代理(如 RabbitMQ) 修改 `WebSocketConfig`: ```java @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay("/topic") .setRelayHost("localhost") .setRelayPort(61613); registry.setApplicationDestinationPrefixes("/app"); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值