Springboot集成WebSocket与定时任务

测试工具推荐使用 http://ws.douqq.com/
网上有很多关于WebSocket用法的文章,但是有一些内容错误或者代码不全的,这里做个记录,方便自己记忆。
首先介绍一下业务需求背景,在一个商品购物系统中,某些订单需要运营后台进行审核通过后才可进行提货等后续操作,这就对及时的获取到是否存在待审核订单有较高的要求;这样就需要后端与前端通知做到紧密互动,在此就不介绍前端轮询请求后端数据与WebSocket方式的优缺点对比了。
需要向前端有审批权限的管理员发送通知的有已下三个场景
1. 运营后台管理员登陆
2. 出现新的待审核订单时
3. 10分钟内仍然存在待审核订单

好了,现在开始搞WebSocket

  1. 在项目引入包文件,pom文件中添加依赖
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-websocket</artifactId>
	</dependency>
  1. 配置文件
@Configuration
@EnableWebSocket
public class WebSocketConfig {
    /**
     * ServerEndpointExporter 作用
     *
     * 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint
     *
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
  1. 创建一个WsServerEndpoint类,用来服务端与客户端进行交互
/**
 * 前后端交互的类实现消息的接收推送(自己发送给自己)
 * 
 * @ServerEndpoint(value = "/websocket/{name}") 前端通过此URI和后端交互,建立连接
 */
@ServerEndpoint("/websocket/{name}")
@Slf4j
@Component
public class WsServerEndpoint {
    /**
     *  与某个客户端的连接对话,需要通过它来给客户端发送消息
     */
    private Session session;
    /**
     * 标识当前连接客户端的用户名
     */
    private String name;
    /**
     *  用于存所有的连接服务的客户端,这个对象存储是安全的
     */
    private static ConcurrentHashMap<String, WsServerEndpoint> webSocketSet = new ConcurrentHashMap<>();

    /**
     * 连接成功
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "name") String name) {
        this.session = session;
        this.name = name;
        webSocketSet.put(name, this);
        log.info("[WebSocket] 连接成功,当前用户{}, 当前连接人数为:={}", name, webSocketSet.size());
    }
    /**
     * 连接关闭
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        webSocketSet.remove(this.name);
        log.info("[WebSocket] 退出成功,当前用户{}, 当前连接人数为:={}", name, webSocketSet.size());
    }

    /**
     * 接收到消息
     *
     * @param message
     */
    @OnMessage
    public String onMsg(String message) throws IOException {
        log.info("[WebSocket] 收到消息:{}",message);
        //判断是否需要指定发送,具体规则自定义
        if(message.indexOf("TOUSER") == 0){
            String name = message.substring(message.indexOf("TOUSER")+6,message.indexOf(";"));
            AppointSending(name,message.substring(message.indexOf(";")+1,message.length()));
        }else{
            GroupSending(message);
        }
        return "servet 发送:" + message;
    }
    /**
     * 群发
     * @param message
     */
    public void GroupSending(String message){
        for (String name : webSocketSet.keySet()){
            try {
                webSocketSet.get(name).session.getBasicRemote().sendText(message);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    /**
     * 指定发送
     * @param name
     * @param message
     */
    public void AppointSending(String name, String message){
        try {
            webSocketSet.get(name).session.getBasicRemote().sendText(message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

好了,上述的基础代码是可以做到简单发送的,但我们是需要登录时发送的,并且要判断审批权限与待审核订单数量,在这里刚开始通过@Resource或者@Autowired注解注入,

@Resource
private OrderService orderService;

使用时发现根本就不行,查找相关资料得知WebSocket是多例,而springboot采用的是单例方式,所以呢,webSocket不归springboot管,解决方式(只介绍一下我使用的方式,感兴趣的小伙伴可以在网上查一下,有很多)

  1. 创建一个SpringUtil文件代码如下
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

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

    public ApplicationContext getApplicationContext(){
        return applicationContext;
    }

    public static Object getBean(String beanName){
        return applicationContext.getBean(beanName);
    }

    public static <T> T getBean(Class<T> clazz){
        return (T)applicationContext.getBean(clazz);
    }
}

  1. 在WsServerEndpoint类中调用
private OrderService orderService = SpringUtil.getBean(OrderService.class);

接下来就可以愉快的使用注入的service了

登陆时使用onOpen方法创建连接的时候进行验证判断发送信息,这样就OK了,完美!
但是但是但是,重要转折说三个但是,你以为这样就可以了吗?真正的坑在后面,在编写订单状态变更触发时通过引入WsServerEndpoint 是不可用的!启动异常,创建bean失败!而同时运行的定时任务没有任何问题,正常发送!那么显而易见,将触发挪移到定时任务中5秒钟查看一次是否存在新的触发,当然了,这里使用的是redis存储状态!运行成功,自测成功,发布测试环境成功!然而,高兴的太早,在linux测试环境发布启动时偶尔才会成功,其他均是启动异常,创建bean失败!这才是最大的坑,本地没有任何问题,linux服务器环境异常,还是偶发成功!经过多次验证得到结果
1. 测试环境发布成功时定时任务经常是不执行的,将这个定时任务内容注释之后启动成功通知成功
2. 将WsServerEndpoint中注入service注释,开启定时任务,启动成功通知成功

那么最终的问题终于确认了,就是
WsServerEndpoint类注入service与被引用不能同时存在!

根据需求也没有别的办法只能全部迁入到定时任务中,WsServerEndpoint类中不注入service
下面为定时任务部分简易代码

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * <p>WebSocket通知定时发送</p>
 **/
@Slf4j
@Component
@EnableScheduling
public class WebSocketNoticeTask {
    private static final String WEBSOCKET_FIRST_CACHEKEY = "WEBSOCKET_FIRST";
    private static final String WEBSOCKET_CACHEKEY = "websocket_";
    private static final String WEBSOCKET_LOGIN_CACHEKEY = "websocket_login";

    @Resource
    private OrderService orderService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // WebSocket通知(每10分钟发送一次通知)
    @Scheduled(cron = "${per10Minute.cron}")
    public void webSocketNoticeTask() {
        log.info("WebSocket通知开始");
        LambdaQueryWrapper<OrderEntity> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderEntity::getStatus, OrderStatus.CHECKED.getStatus());// 待复核(已审核)
        int count = orderService.count(queryWrapper);
        if (count > 0){
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("count", count);
            ConcurrentHashMap<String, WsServerEndpoint> webSocketHashMap = WsServerEndpoint.getWebSocketHashMap();
            for (Map.Entry<String, WsServerEndpoint> entry : webSocketHashMap.entrySet()) {
                boolean result = this.validAuthority(entry.getKey());
                if (!result){
                    continue;
                }
                webSocketHashMap.get(entry.getKey()).AppointSending(entry.getKey(), jsonObject.toJSONString());
            }
        }
        log.info("WebSocket通知结束");
    }

    /**
     * 验证权限
     * @return
     */
    private boolean validAuthority(String name){
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        String cacheValue = opsForValue.get(WEBSOCKET_CACHEKEY+name);
        return cacheValue != null && cacheValue.equals("1");
    }
}

现在可以说任务基本完成了,可优化的一个点是:同一账号多点登陆时,只会向最后一个登陆的用户发送通知! 对此只需要对代码进行小小的改动即可

WsServerEndpoint类中添加map集合,并在连接成功是存储name改为存储id,
WebSocketNoticeTask类定时任务中根据map通过登陆用户名获取发送标识ID进行轮询发送

@ServerEndpoint("/websocket/{name}")
@Slf4j
@Component
public class WsServerEndpoint {
    /**
     *  与某个客户端的连接对话,需要通过它来给客户端发送消息
     */
    private Session session;
    /**
     * 标识当前连接客户端的用户名
     */
    private String name;
    /**
     *  用于存所有的连接服务的客户端,这个对象存储是安全的
     */
    private static ConcurrentHashMap<String, WsServerEndpoint> webSocketSet = new ConcurrentHashMap<>();

    private static HashMap<String, Set<String>> nameMap = new HashMap<>();

    /**
     * 连接成功
     *
     * @param session
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "name") String name) {
        this.session = session;
        this.name = name;
        Set<String> set = new HashSet<>();
        if (nameMap.containsKey(name)){
            set = nameMap.get(name);
        }
        set.add(session.getId());
        nameMap.put(name, set);
        webSocketSet.put(session.getId(), this);
        log.info("[WebSocket] 连接成功,当前用户{}, 当前连接人数为:={}", name, webSocketSet.size());
    }
    /**
     * 连接关闭
     *
     * @param session
     */
    @OnClose
    public void onClose(Session session) {
        webSocketSet.remove(session.getId());
        log.info("[WebSocket] 退出成功,当前用户{}, 当前连接人数为:={}", name, webSocketSet.size());
    }

    /**
     * 接收到消息
     *
     * @param message
     */
    @OnMessage
    public String onMsg(String message) throws IOException {
        log.info("[WebSocket] 收到消息:{}",message);
        //判断是否需要指定发送,具体规则自定义
        if(message.indexOf("TOUSER") == 0){
            String name = message.substring(message.indexOf("TOUSER")+6,message.indexOf(";"));
            AppointSending(name,message.substring(message.indexOf(";")+1,message.length()));
        }else{
            GroupSending(message);
        }
        return "servet 发送:" + message;
    }
    /**
     * 群发
     * @param message
     */
    public void GroupSending(String message){
        for (String id : webSocketSet.keySet()){
            try {
                webSocketSet.get(id).session.getBasicRemote().sendText(message);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    /**
     * 指定发送
     * @param name
     * @param message
     */
    public void AppointSending(String name, String message){
        try {
            webSocketSet.get(name).session.getBasicRemote().sendText(message);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    public static ConcurrentHashMap<String, WsServerEndpoint> getWebSocketHashMap() {
        return webSocketSet;
    }

    public static HashMap<String, Set<String>> getNameMap(){
        return nameMap;
    }
@Slf4j
@Component
@EnableScheduling
public class WebSocketNoticeTask {
    private static final String WEBSOCKET_FIRST_CACHEKEY = "WEBSOCKET_FIRST";
    private static final String WEBSOCKET_CACHEKEY = "websocket_";
    private static final String WEBSOCKET_LOGIN_CACHEKEY = "websocket_login";

    @Resource
    private OrderService orderService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // WebSocket通知(每10分钟发送一次通知)
    @Scheduled(cron = "${per10Minute.cron}")
    public void webSocketNoticeTask() {
        log.info("WebSocket通知开始");
        HashMap<String, Set<String>> nameHashMap = WsServerEndpoint.getNameMap();
        ConcurrentHashMap<String, WsServerEndpoint> webSocketHashMap = WsServerEndpoint.getWebSocketHashMap();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("count", 1);
        for (Map.Entry<String, Set<String>> entry : nameHashMap.entrySet()) {
            boolean result = this.validAuthority(entry.getKey());
            if (!result){
                continue;
            }
            Set<String> set = entry.getValue();
            for (String id : set) {
                webSocketHashMap.get(id).AppointSending(id, jsonObject.toJSONString());
            }
        }
        log.info("WebSocket通知结束");
    }
    /**
     * 验证权限
     * @return
     */
    private boolean validAuthority(String name){
        ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
        String cacheValue = opsForValue.get(WEBSOCKET_CACHEKEY+name);
        return cacheValue != null && cacheValue.equals("1");
    }
}

到这里算是基本结束了,后续如果有时间再深入研究一下!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值