spring cloud 同一服务多实例 websocket跨实例无法共享Session 的解决

思路>>使用redis发布消息,通知其他实例,查询符合条件的Session用于发送消息

复制代码
package com.ruoyi.common.redis.message;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

/**
 * ApplicationContext 本身就实现了 ApplicationEventPublisher 接口
 */
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext context;

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }

    public static ApplicationEventPublisher getEventPublisher() {
        return context;
    }
}

package com.ruoyi.common.redis.message;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;

@Configuration
public class RedisMessageConfig {
    /**
     * 创建消息监听器容器
     * @param connectionFactory Redis连接工厂
     * @param listenerAdapter 消息监听器适配器
     * @return RedisMessageListenerContainer
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 将监听器适配器添加到容器,并订阅指定频道
        container.addMessageListener(listenerAdapter, new PatternTopic("*"));
        // 你也可以订阅多个频道,或者使用PatternTopic进行模式匹配订阅
        // container.addMessageListener(listenerAdapter, new PatternTopic("news.*"));
        return container;
    }

    /**
     * 将我们实现了MessageListener接口的订阅者包装成MessageListenerAdapter
     * Spring会默认寻找名为"onMessage"的方法
     * @param subscriber 我们的订阅者实例
     * @return MessageListenerAdapter
     */
    @Bean
    public MessageListenerAdapter listenerAdapter() {
        return new MessageListenerAdapter(new  RedisMessageSubscriber(),"onMessage");
    }
}

package com.ruoyi.common.redis.message;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisMessagePublisher {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 发布消息
     * @param channel
     * @param message
     */
    public void publish(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
    }
}

package com.ruoyi.common.redis.message;

import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.message.event.Commemorate3dCemeteryEvent;
import com.ruoyi.common.redis.message.event.Commemorate3dHallEvent;
import com.ruoyi.common.redis.message.event.KinMemberChangeEvent;
import com.ruoyi.common.redis.message.event.MemorialHallEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class RedisMessageSubscriber implements  MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        ApplicationEventPublisher eventPublisher = ApplicationContextProvider.getEventPublisher();


        String actualChannel = new String(message.getChannel(), StandardCharsets.UTF_8);

        System.out.println("redisMessage频道: " + actualChannel);
        System.out.println("redisMessage消息: " + message);
        //System.out.println("消息类型: " + message.getClass().getSimpleName());

        //举例 module:business:key websocket:kinMemberChange:kinMemberChange-userid
        String[] split = StringUtils.split(actualChannel, ':');
        if (split.length != 3) {
            log.error("redis onMessage channel error");
        }
        if(StringUtils.equals(split[0], "websocket")){
            //同一模块部署多实例的时候,任何一个模块发送websocket消息,都要通知其他实例,在内存中找session对象,进行发送消息
            if(StringUtils.equals(split[1], "kinMemberChange")){
                //其他实例,kinMemberChange 业务,发布了新消息
                String[] split1 = StringUtils.split(split[2], "_");
                eventPublisher.publishEvent(new KinMemberChangeEvent(this,split1[0],split1[1],""));
            }else if(StringUtils.equals(split[1], "commemorate3dCemetery")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new Commemorate3dCemeteryEvent(this,split1[0],split1[1],messageBody));
            }else if(StringUtils.equals(split[1], "commemorate3dHall")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new Commemorate3dHallEvent(this,split1[0],split1[1],messageBody));
            }else if(StringUtils.equals(split[1], "memorialHall")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new MemorialHallEvent(this,split1[0],split1[1],messageBody));
            }
        }
    }


    /**
     * 去除字符串首尾的多余引号
     */
    private String removeExtraQuotes(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }

        // 去除首尾的双引号
        if (str.startsWith("\"") && str.endsWith("\"")) {
            return str.substring(1, str.length() - 1);
        }

        // 去除首尾的单引号
        if (str.startsWith("'") && str.endsWith("'")) {
            return str.substring(1, str.length() - 1);
        }

        return str;
    }
}

在websocket收到消息时,发布redis消息,通知所有实例

复制代码
package com.ruoyi.foundation.apicontroller;

import com.google.gson.Gson;
import com.ruoyi.common.redis.message.RedisMessagePublisher;
import com.ruoyi.foundation.apicontroller.req.MemorialHallWebsocketReq;
import com.ruoyi.foundation.event.dto.AnimationBroadcastDto;
import com.ruoyi.foundation.webSocket.WebsocketUtil;
import io.seata.common.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
@ServerEndpoint(value = "/api/mingmenCommemorate3dHallWebsocket/{commemorate3d}/{userId}")
public class MmCommemorate3dHallWebsocketController {
    /**
     * 保存每日一念纪念馆中当前在线的用户ID
     */
    public static final Map<String, List<String>> memorialHallUsers = new ConcurrentHashMap<>();

    private Gson gson=new Gson();
    private static ApplicationEventPublisher eventPublisher;

    private static RedisMessagePublisher redisMessagePublisher;

    @Autowired
    public void setEventPublisher(ApplicationEventPublisher publisher) {
        MmCommemorate3dHallWebsocketController.eventPublisher = publisher;
    }

    @Autowired
    public void setRedisMessagePublisher(RedisMessagePublisher publisher) {
        MmCommemorate3dHallWebsocketController.redisMessagePublisher = publisher;
    }

    /*public MmMemorialHallWebsocketController() {
        // 从 Spring 容器中获取 ApplicationEventPublisher
        this.eventPublisher = SpringContextUtil.getBean(ApplicationEventPublisher.class);
    }*/

    @OnOpen
    public void onOpen(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session) {
        WebsocketUtil.addSession("commemorate3d:"+userId, session);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if (strings == null){
            List<String> list=new ArrayList<>();
            list.add("commemorate3d:"+userId);
            memorialHallUsers.put(commemorate3d,list);
        }else{
            strings.add("commemorate3d:"+userId);
        }
    }

    @OnClose
    public void onClose(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session) {
        WebsocketUtil.removeSession("commemorate3d:"+userId);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings != null){
            strings.remove("commemorate3d:"+userId);
        }
    }

    @OnMessage
    public void onMessage(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session, String message) {
        /*System.out.println(dailyMissId);
        System.out.println(userId);
        System.out.println(session);
        System.out.println(message);*/

        //发送redis消息,使消息扩散到每个spring boot 实例
        redisMessagePublisher.publish("websocket:commemorate3dHall:"+commemorate3d + "_" + userId,message);

        /*MemorialHallWebsocketReq memorialHallWebsocketReq = gson.fromJson(message, MemorialHallWebsocketReq.class);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings == null || strings.isEmpty()){
            return;
        }

        Set<String> collect = strings.stream().filter(userId1 -> !StringUtils.equals(userId1, "commemorate3d:"+userId)).collect(Collectors.toSet());

        //对在线的用户广播前,记录动效已经播放
        if(StringUtils.isNotBlank(memorialHallWebsocketReq.getMingmenSacrificeRecordId()) && collect.size() > 0){
            List<Long> collect1 = collect.stream().map(u -> Long.valueOf(u.split(":")[1])).collect(Collectors.toList());
            eventPublisher.publishEvent(new AnimationBroadcastDto(this,Long.valueOf(memorialHallWebsocketReq.getMingmenSacrificeRecordId()),collect1));
        }


        //对同纪念馆的在线用户进行广播
        WebsocketUtil.sendMessageForUsers(collect,message);*/
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throwable.printStackTrace();
    }

    /**
     * 消息来自redis,通过redis可使同一模块所有实例都收到消息
     * @param commemorate3d
     * @param userId
     * @param message
     */
    public void onMessageFromRedis(String commemorate3d,String userId,String message){
        MemorialHallWebsocketReq memorialHallWebsocketReq = gson.fromJson(message, MemorialHallWebsocketReq.class);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings == null || strings.isEmpty()){
            return;
        }

        Set<String> collect = strings.stream().filter(userId1 -> !StringUtils.equals(userId1, "commemorate3d:"+userId)).collect(Collectors.toSet());

        //对在线的用户广播前,记录动效已经播放
        if(StringUtils.isNotBlank(memorialHallWebsocketReq.getMingmenSacrificeRecordId()) && collect.size() > 0){
            List<Long> collect1 = collect.stream().map(u -> Long.valueOf(u.split(":")[1])).collect(Collectors.toList());
            eventPublisher.publishEvent(new AnimationBroadcastDto(this,Long.valueOf(memorialHallWebsocketReq.getMingmenSacrificeRecordId()),collect1));
        }


        //对同纪念馆的在线用户进行广播
        WebsocketUtil.sendMessageForUsers(collect,message);
    }
}

实例收到redis消息后,转化为ApplicationEventPublisher 消息,从基础层调用业务层

复制代码
package com.ruoyi.foundation.redisevents;

import com.ruoyi.common.redis.message.event.Commemorate3dHallEvent;
import com.ruoyi.foundation.apicontroller.MmCommemorate3dHallWebsocketController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class Commemorate3dHallListen {
    @Async
    @EventListener
    public void commemorate3dCemetery(Commemorate3dHallEvent event) {
        MmCommemorate3dHallWebsocketController mmCommemorate3dHallWebsocketController = new MmCommemorate3dHallWebsocketController();
        mmCommemorate3dHallWebsocketController.onMessageFromRedis(event.getCommemorate3d(),event.getUserId(),event.getMessage());
    }
}
相关推荐
奋进的芋圆39 分钟前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin1 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20051 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉1 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国1 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882482 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈2 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_992 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
lhrimperial2 小时前
企业级消息中心架构设计与实践:多渠道统一推送平台
spring cloud·中间件·系统架构
沛沛老爹2 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理