踩坑篇之WebSocket实现类中无法使用@Autowired注入对象

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 踩坑篇之WebSocket实现类中无法使用@Autowired注入对象

接下来我讲讲我踩坑的经历吧!


package cn.donglifeng.shop.socket.endpoin;
import cn.donglifeng.shop.common.context.SpringBeanContext;
import cn.donglifeng.shop.common.redis.RedisUtil;
import cn.donglifeng.shop.socket.config.WebSocketConfiguration;
import cn.donglifeng.shop.socket.util.WebSocketEndpointTool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @author JanYork
 * @date 2023/3/14 11:36
 * @description WebSocket服务端点
 */
@ServerEndpoint(value = "/websocket/{uid}",configurator = WebSocketConfiguration.class)
@Component
@Slf4j
public class WebSocketEndpoint {
    @Resource
    public RedisUtil redisUtil;
    /**
     * 连接建立成功调用的方法
     *
     * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     * @param uid     用户id
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("uid") String uid) {
        try {
            redisUtil.socketOnline(Long.parseLong(uid));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 收到客户端消息后调用的方法
     */
    @OnMessage
    public void onMessage(String message) {
        if (StringUtils.hasLength(message)) {
            //TODO 业务逻辑
        } else {
        }
    }
    /**
     * 连接错误调用的方法
     *
     * @param error 错误信息
     */
    @OnError
    public void onError(Throwable error) {
        error.printStackTrace();
    }
    /**
     * 连接关闭调用的方法
     *
     * @param session 会话
     * @param uid     用户id
     */
    @OnClose
    public void onClose(Session session, @PathParam("uid") String uid) {
    }
    /**
     * @return 在线人数
     */
    public AtomicInteger getOnlineCount() {
        return new AtomicInteger(redisUtil.countSocketOnline().intValue());
    }
}


上面是一个很简单的WebSocket端点服务类。

我打算使用RedisBitmap来做连接人数统计。


空指针?


@Resource
public RedisUtil redisUtil;


我直接注入我封装的Redis工具类,然后自信满满的开始测试。

结果.....



???

居然空指针???什么情况?

我是百思难得其解呀,因为这个类本身也是一个Bean,使用了@Component注解。


寻找答案


我开始使用万能的浏览器搜索。

于是在一番搜寻后,在CSDN东拼西凑,综合找到以下答案:

首先,使用了@ServerEndpoint注解的类中使用@Resource@Autowired注入都会失败,并且报出空指针异常。

原因是WebSocket服务是线程安全的,那么当我们去发起一个ws连接时,就会创建一个端点对象。

那么问题就在这了,根据CSDN上的说明,WebSocket服务是多对象的,不是单例的。

而我们的SpringBean默认就是单例的,在非单例类中注入一个单例的Bean是冲突的。

而且我虽然使用@Component注解了这个类,但是WebSocket的端点仍然不是单例的,这个是必须的,端点服务不可能单例。

来自CSDN

@Autowired注解注入对象是在启动的时候就把对象注入,而不是在使用A对象时才把A需要的B对象注入到A中。

WebSocket在刚刚有说到,有连接时才实例化对象,而且有多个连接就有多个。


如何解决?


知道原因还不好解决吗?我们开发的适合,基本上很常见的遇到要在非Bean的类中使用Bean,因为不被Spring容器所管理的类中是无法注入Bean对象的,所以我们需要去使用一个上下文类,在一开始就将Spring中所有的Bean静态化到上下文类中。


如何实现?


定义一个类,实现ApplicationContextAware接口:


public class SpringBeanContext implements ApplicationContextAware


不过需要注意的是!这个类也必须要是Bean,不如无法获取到SpringApplicationContext


@Component
public class SpringBeanContext implements ApplicationContextAware {
    private static ApplicationContext context;
    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}


重写他的setApplicationContext方法,将ApplicationContext赋值给本类静态的属性。

此时,当我们启动程序,Spring中的Bean对象就全部会被context获取到。

然后我们还需要写从上下文中获取Bean的方法,我就直接丢代码了:


package cn.donglifeng.shop.common.context;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
/**
 * @author JanYork
 * @date 2023/3/8 9:33
 * @description SpringBean上下文
 */
@Component
public class SpringBeanContext implements ApplicationContextAware {
    private static ApplicationContext context;
    @Override
    public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
    /**
     * 获取上下文
     *
     * @return 上下文对象
     */
    public static ApplicationContext getContext() {
        return context;
    }
    /**
     * 根据beanName获取bean
     *
     * @param beanName bean名称
     * @return bean对象
     */
    public Object getBean(String beanName) {
        return context.getBean(beanName);
    }
    /**
     * 根据beanName和类型获取bean
     *
     * @param beanName bean名称
     * @param clazz    bean类型
     * @param <T>      bean类型
     * @return bean对象
     */
    public <T> T getBean(String beanName, Class<T> clazz) {
        return context.getBean(beanName, clazz);
    }
    /**
     * 根据类型获取bean
     *
     * @param clazz bean类型
     * @param <T>   bean类型
     * @return bean对象
     */
    public <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }
}


解决效果


/**
     * 连接建立成功调用的方法
     *
     * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
     * @param uid     用户id
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("uid") String uid) {
        try {
            RedisUtil bean = SpringBeanContext.getContext().getBean(RedisUtil.class);
            bean.socketCache(uid, session);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


这里我通过上下文类去获取到Bean对象,然后测试连接成功了。


扩展知识


注意!我这里有坑,别踩着了,我测试的适合数据还是写入失败了,我这里是想将SocketSession丢到Redis里面实现分布式环境对象共享(小小的尝试)。


bean.socketCache(uid, session);


显然是不行的,序列化会报错,因为:



看他的源码,他没有去实现Serializable接口,是不能被序列化的!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
Java Spring
websocket @Autowired注入为null问题的解决办法
websocket @Autowired注入为null问题的解决办法
|
7月前
|
网络协议 前端开发 Java
SpringBoot 整合 WebSocket
WebSocket是基于TCP协议的一种网络协议,它实现了浏览器与服务器全双工通信,支持客户端和服务端之间相互发送信息。在有WebSocket之前,如果服务端数据发生了改变,客户端想知道的话,只能采用定时轮询的方式去服务端获取,这种方式很大程度上增大了服务器端的压力,有了WebSocket之后,如果服务端数据发生改变,可以立即通知客户端,客户端就不用轮询去换取,降低了服务器的压力。目前主流的浏览器都已经支持WebSocket协议了。
SpringBoot 整合 WebSocket
|
5月前
|
前端开发 网络协议 JavaScript
在Spring Boot中实现基于WebSocket的实时通信
在Spring Boot中实现基于WebSocket的实时通信
|
2月前
|
开发框架 前端开发 网络协议
Spring Boot结合Netty和WebSocket,实现后台向前端实时推送信息
【10月更文挑战第18天】 在现代互联网应用中,实时通信变得越来越重要。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为客户端和服务器之间的实时数据传输提供了一种高效的解决方案。Netty作为一个高性能、事件驱动的NIO框架,它基于Java NIO实现了异步和事件驱动的网络应用程序。Spring Boot是一个基于Spring框架的微服务开发框架,它提供了许多开箱即用的功能和简化配置的机制。本文将详细介绍如何使用Spring Boot集成Netty和WebSocket,实现后台向前端推送信息的功能。
466 1
|
2月前
|
前端开发 Java C++
RSocket vs WebSocket:Spring Boot 3.3 中的两大实时通信利器
本文介绍了在 Spring Boot 3.3 中使用 RSocket 和 WebSocket 实现实时通信的方法。RSocket 是一种高效的网络通信协议,支持多种通信模式,适用于微服务和流式数据传输。WebSocket 则是一种标准协议,支持全双工通信,适合实时数据更新场景。文章通过一个完整的示例,展示了如何配置项目、实现前后端交互和消息传递,并提供了详细的代码示例。通过这些技术,可以大幅提升系统的响应速度和处理效率。
|
4月前
|
开发框架 网络协议 Java
SpringBoot WebSocket大揭秘:实时通信、高效协作,一文让你彻底解锁!
【8月更文挑战第25天】本文介绍如何在SpringBoot项目中集成WebSocket以实现客户端与服务端的实时通信。首先概述了WebSocket的基本原理及其优势,接着详细阐述了集成步骤:添加依赖、配置WebSocket、定义WebSocket接口及进行测试。通过示例代码展示了整个过程,旨在帮助开发者更好地理解和应用这一技术。
395 1
|
4月前
|
小程序 Java API
springboot 微信小程序整合websocket,实现发送提醒消息
springboot 微信小程序整合websocket,实现发送提醒消息
|
4月前
|
JavaScript 前端开发 网络协议
WebSocket在Java Spring Boot+Vue框架中实现消息推送功能
在现代Web应用中,实时消息提醒是一项非常重要的功能,能够极大地提升用户体验。WebSocket作为一种在单个TCP连接上进行全双工通信的协议,为实现实时消息提醒提供了高效且低延迟的解决方案。本文将详细介绍如何在Java Spring Boot后端和Vue前端框架中利用WebSocket实现消息提醒功能。
208 0
|
6月前
|
前端开发 JavaScript 安全
集成WebSocket在Spring Boot中可以用于实现实时的双向通信
集成WebSocket在Spring Boot中可以用于实现实时的双向通信
105 4
|
5月前
|
监控 网络协议 Java
如何在Spring Boot中使用WebSocket
如何在Spring Boot中使用WebSocket