SpringBoot+WebSocket+Session共享

news2024/11/24 6:56:55

前言

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端

一、为什么需要WebSocket

HTTP 是基于请求响应式的,即通信只能由客户端发起,服务端做出响应,无状态,无连接。

无状态: 每次连接只处理一个请求,请求结束后断开连接。

无连接: 对于事务处理没有记忆能力,服务器不知道客户端是什么状态。

通过HTTP实现即时通讯,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源,因为必须不停连接,或者 HTTP 连接始终打开。

WebSocket的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。

WebSocket特点

  • (1)建立在 TCP 协议之上,服务器端的实现比较容易。

  • (2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种
    HTTP 代理服务器。

  • (3)数据格式比较轻量,性能开销小,通信高效。

  • (4)可以发送文本,也可以发送二进制数据。

  • (5)没有同源限制,客户端可以与任意服务器通信。

  • (6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

二、SpringBoot整合WebSocket

1.maven依赖

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

2.WebSocketConfig

启用WebSocket的支持

@Configuration
public class WebSocketConfig {
    /**
     * 自动注册使用@ServerEndpoint注解声明的websocket endpoint
     * 2022/2/14
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3.WebSocketServer

因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller

直接@ServerEndpoint(“/websocket/{userId}”) 、@Component启用即可,然后在里面实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等方法。

集群版(多个ws节点)还需要借助mysql或者redis等进行处理,改造对应的sendMessage方法即可。

import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
 
 
@ServerEndpoint("/imserver/{userId}")
@Component
public class WebSocketServer {
 
    static Log log=LogFactory.get(WebSocketServer.class);
    /**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/
    private static int onlineCount = 0;
    /**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/
    private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
    /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
    private Session session;
    /**接收userId*/
    private String userId="";
 
    /**
     * 连接建立成功调用的方法*/
    @OnOpen
    public void onOpen(Session session,@PathParam("userId") String userId) {
        this.session = session;
        this.userId=userId;
        if(webSocketMap.containsKey(userId)){
            webSocketMap.remove(userId);
            webSocketMap.put(userId,this);
            //加入set中
        }else{
            webSocketMap.put(userId,this);
            //加入set中
            addOnlineCount();
            //在线数加1
        }
 
        log.info("用户连接:"+userId+",当前在线人数为:" + getOnlineCount());
 
        try {
            sendMessage("连接成功");
        } catch (IOException e) {
            log.error("用户:"+userId+",网络异常!!!!!!");
        }
    }
 
    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if(webSocketMap.containsKey(userId)){
            webSocketMap.remove(userId);
            //从set中删除
            subOnlineCount();
        }
        log.info("用户退出:"+userId+",当前在线人数为:" + getOnlineCount());
    }
 
    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("用户消息:"+userId+",报文:"+message);
        //可以群发消息
        //消息保存到数据库、redis
        if(StringUtils.isNotBlank(message)){
            try {
                //解析发送的报文
                JSONObject jsonObject = JSON.parseObject(message);
                //追加发送人(防止串改)
                jsonObject.put("fromUserId",this.userId);
                String toUserId=jsonObject.getString("toUserId");
                //传送给对应toUserId用户的websocket
                if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){
                    webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());
                }else{
                    log.error("请求的userId:"+toUserId+"不在该服务器上");
                    //否则不在这个服务器上,发送到mysql或者redis
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
 
    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("用户错误:"+this.userId+",原因:"+error.getMessage());
        error.printStackTrace();
    }
    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }
 
 
    /**
     * 发送自定义消息
     * */
    public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {
        log.info("发送消息到:"+userId+",报文:"+message);
        if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){
            webSocketMap.get(userId).sendMessage(message);
        }else{
            log.error("用户"+userId+",不在线!");
        }
    }
 
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
 
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }
 
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

4.集群版

在使用websocket的时候会面对session共享的问题

在这里我们通过Redis的监听订阅模式来实现session共享,当有连接加入时,将连接发送到队列中然后向订阅该队列的服务下发,由存储该连接的服务进行逻辑处理

session共享的业务逻辑如下图所示
在这里插入图片描述

4.1 Redis发布订阅模式实现

前言

Redis可以通过发布订阅模式、轮询机制实现消息队列。

由于没有消息持久化与 ACK 的保证,所以,Redis 的发布订阅功能并不可靠。这也就导致了它的应用场景很有限,建议用于实时与可靠性要求不高的场景。

一、Redis发布订阅

​​​​​​1. Redis发布订阅模式实现原理

服务器中维护着一个pubsub_channels字典,所有的频道和订阅关系都存储在这里。字典的键为频道的名称,而值为订阅频道的客户端链表。

1> 当有新的客户端订阅某个频道时,会发生两种情况中的一种:

(1)如果频道已经存在,则新的客户端会添加到pubsub_channels对应频道的链表末尾

(2)如果频道原本不存在,则会为频道创建一个键,该客户端成为链表的第一个元素

2> 当一个客户端退订一个频道的时候:

pubsub_channels对应键的链表会删除该客户端

3> 发送信息

服务器会遍历pubsub_channels中对应键的链表,向每一个客户端发送信息

服务器还维护着一个pubsub_patterns链表,链表的pattern属性记录了被订阅的模式,而client属性记录了订阅模式的客户端

  1. 当有新的客户端订阅某个模式的时,会进行如下步骤:

(1)创建一个链表节点,pattern属性记录订阅的模式,client记录订阅模式的客户端

(2)将这个链表节点添加到pubsub_patterns链表中

  1. 当一个客户端退订某一个模式的时候:

服务器遍历pubsob_patterns找到对应的pattern同时也是对应该client客户端的节点,将改节点删除

  1. 发送信息

服务器遍历pubsub_channels,查找与channels频道相匹配的模式麻将消息发送给订阅了这些模式的客户端。

2.引入依赖
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
3.redis配置
public class RedisConfig {
    //Key的过期时间
    private Duration timeToLive = Duration.ofDays(1);
 
    /**
     * redis模板,存储关键字是字符串,值jackson2JsonRedisSerializer是序列化后的值
     *
     * @param
     * @return org.springframework.data.redis.core.RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
 
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        RedisSerializer redisSerializer = new StringRedisSerializer();
        //key
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        //value
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
 
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
 
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        return RedisCacheConfiguration.
                defaultCacheConfig().
                entryTtl(this.timeToLive).			//Key过期时间 1天
                serializeKeysWith(RedisSerializationContext.SerializationPair.
                fromSerializer(new StringRedisSerializer())).
                serializeValuesWith(RedisSerializationContext.SerializationPair.
                        fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

4.redis监听
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;
 
import java.util.concurrent.CountDownLatch;
 
 
@Configuration
public class RedisMessageListener {
    /**
     * 创建连接工厂
     *
     * @param connectionFactory
     * @param listenerAdapter
     * @return
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                                   MessageListenerAdapter listenerAdapter,
                                                   MessageListenerAdapter listenerAdapterWang,
                                                   MessageListenerAdapter listenerAdapterTest2) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //(不同的监听器可以收到同一个频道的信息)接受消息的频道
        container.addMessageListener(listenerAdapter, new PatternTopic("phone"));
 
        container.addMessageListener(listenerAdapterWang, new PatternTopic("phone"));
 
        container.addMessageListener(listenerAdapterTest2, new PatternTopic("phoneTest2"));
        return container;
    }
 
 
    /**
     * 绑定消息监听者和接收监听的方法
     *
     * @param receiver
     * @return
     */
    @Bean
    public MessageListenerAdapter listenerAdapter(ReceiverRedisMessage receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }
 
    @Bean
    public MessageListenerAdapter listenerAdapterWang(ReceiverRedisMessage receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessageWang");
    }
   /**
     * 绑定消息监听者和接收监听的方法
     *
     * @param receiver
     * @return
     */
    @Bean
    public MessageListenerAdapter listenerAdapterTest2(ReceiverRedisMessage receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage2");
    }
 
    /**
     * 注册订阅者
     *
     * @param latch
     * @return
     */
    @Bean
    ReceiverRedisMessage receiver(CountDownLatch latch) {
        return new ReceiverRedisMessage(latch);
    }
 
 
    /**
     * 计数器,用来控制线程
     *
     * @return
     */
    @Bean
    public CountDownLatch latch() {
        return new CountDownLatch(1);//指定了计数的次数 1
    }
}
5.redis消息接收类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
 
import java.util.concurrent.CountDownLatch;
 
 
public class ReceiverRedisMessage {
    private CountDownLatch latch;
 
    @Autowired
    public ReceiverRedisMessage(CountDownLatch latch) {
        this.latch = latch;
    }
 
 
    /**
     * 队列消息接收方法
     *
     * @param jsonMsg
     */
    public void receiveMessage(String jsonMsg) {
        log.info("[开始消费REDIS消息队列phone数据...]");
        try {
            System.out.println(jsonMsg);
            log.info("[消费REDIS消息队列phone数据成功.]");
        } catch (Exception e) {
            log.error("[消费REDIS消息队列phone数据失败,失败信息:{}]", e.getMessage());
        }
        latch.countDown();
    }
 
 
    public void receiveMessageWang(String jsonMsg) {
        log.info("[开始消费REDIS消息队列phone数据...]");
        try {
            System.out.println(jsonMsg);
            log.info("[消费REDIS消息队列phone数据成功.]");
        } catch (Exception e) {
            log.error("[消费REDIS消息队列phone数据失败,失败信息:{}]", e.getMessage());
        }
        latch.countDown();
    }
 
    /**
     * 队列消息接收方法
     *
     * @param jsonMsg
     */
    public void receiveMessage2(String jsonMsg) {
        log.info("[开始消费REDIS消息队列phoneTest2数据...]");
        try {
            System.out.println(jsonMsg);
            /**
             *  此处执行自己代码逻辑 操作数据库等
             */
 
            log.info("[消费REDIS消息队列phoneTest2数据成功.]");
        } catch (Exception e) {
            log.error("[消费REDIS消息队列phoneTest2数据失败,失败信息:{}]", e.getMessage());
        }
        latch.countDown();
    }
 
}
6.代码测试
  public void redisTest() {
        //这个是测试同一个频道,不同的订阅者收到相同的信息,“phone”也就是topic也可以理解为频道
        redisTemplate.convertAndSend("phone", "223333");
        //这个phoneTest2是另外的一个频道
      //  redisTemplate.convertAndSend("phoneTest2", "34555665");
    } 

二、Redis轮询机制

1.原理

Redis的列表类型键可以用来实现队列,并且支持阻塞式读取,可以实现一个高性能的优先队列, 在Redis中,List类型是按照插入顺序排序的字符串链表。和数据结构中的普通链表一样,我们可以在其头部(left)和尾部(right)添加新的元素。在插入时,如果该键并不存在,Redis将为该键创建一个新的链表。与此相反,如果链表中所有的元素均被移除,那么该键也将会被从数据库中删除。List中可以包含的最大元素数量是4294967295。从元素插入和删除的效率视角来看,如果我们是在链表的两头插入或删除元素,这将会是非常高效的操作,即使链表中已经存储了百万条记录,该操作也可以在常量时间内完成。

2.生产者
  public R saveUserTicket(String phoneNum) {
        redisTemplate.opsForList().leftPush("ticket:Data", phoneNum);
        return R.ok();
    }
3.消费者
 @Scheduled(fixedRate = 1)
    public synchronized void consumer() {
            String message = redisTemplate.opsForList().rightPop("ticket:Data", 5, TimeUnit.SECONDS);
            if (!StringUtils.isEmpty(message)){
           //数据库操作
            }
    }
4.优化

如上述代码,如果此时队列为空,消费者依然会频繁拉取数据,造成CPU空转,不仅占用CPU资源还对Redis造成压力。因此当队列为空时我们可以休眠一段时间,再进行拉取。

实现如下

     @Scheduled(fixedRate = 1)
    public synchronized void consumer() throws InterruptedException {
        long a = redisTemplate.opsForList().size("ticket:Data");
        if (a == 0) {
            TimeUnit.SECONDS.sleep(1);//等待时间
        }
            String message = redisTemplate.opsForList().rightPop("ticket:Data", 5, TimeUnit.SECONDS);
            if (!StringUtils.isEmpty(message)){
           //数据库操作
            }

Redis发布订阅模式实现原理

前言

发布订阅系统在我们日常的工作中经常会使用到,这种场景大部分情况我们都是使用消息队列,常用的消息队列有 Kafka,RocketMQ,RabbitMQ,每一种消息队列都有其特性,很多时候我们可能不需要独立部署相应的消息队列,只是简单的使用,而且数据量也不会太大,这种情况下,我们就可以使用 Redis 的 Pub/Sub 模型。

一、Redis发布订阅

Redis 的发布订阅功能主要由 PUBLISH,SUBSCRIBE,PSUBSCRIBE 命令组成,一个或者多个客户端订阅某个或者多个频道,当其他客户端向该频道发送消息的时候,订阅了该频道的客户端都会收到对应的消息。

二、发布订阅模式的基本命令

Psubscribe 命令订阅一个或多个符合给定模式的频道。每个模式以 * 作为匹配符,比如 it* 匹配所有以 it 开头的频道( it.news 、 it.blog 、 it.tweets )

Psubscribe 命令基本语法如下:

PSUBSCRIBE pattern [pattern …]

Redis Pubsub 命令用于查看订阅与发布系统状态,它由数个不同格式的子命令组成。

PUBSUB [argument [argument …]]

Publish 命令用于将信息发送到指定的频道。

Publish 命令基本语法如下:

PUBLISH channel message

Punsubscribe 命令用于退订所有给定模式的频道。

Punsubscribe 命令基本语法如下:

PUNSUBSCRIBE [pattern [pattern …]]

Subscribe 命令用于订阅给定的一个或多个频道的信息。

Subscribe 命令基本语法如下:

SUBSCRIBE channel [channel …]

Unsubscribe 命令用于退订给定的一个或多个频道的信息。

Unsubscribe 命令基本语法如下:

UNSUBSCRIBE channel [channel …]

三、发布订阅模式的实现原理

Redis通过PUBLISH、SUBSCRIBE和PSUBSCRIBE等命令实现了发布和订阅功能。

  • 通过SUBSCRIBE命令订阅某个频道后,redis-server里面维护了一个字典,字典的键就是一个个channel,而字典的值则是一个链表,链表中保存了所有订阅这个channel的客户端。SUBSCRIBE命令的关键,就是将客户端添加到指定channel的订阅链表中。
  • 通过PUBLISH命令向订阅者发送信息,redis-server会使用给定的频道作为键,在他所维护的channel字典中查找记录订阅该频道的所有客户端的链表,通过遍历这个链表,来将信息发布给所有的订阅者
Pub/Sub 底层存储结构

订阅 Channel

在 Redis 的底层结构中,客户端和频道的订阅关系是通过一个字典加链表的结构保存的,形式如下:

在这里插入图片描述

在 Redis 的底层结构中,Redis 服务器结构体中定义了一个 pubsub_channels 字典

struct redisServer {
 
//用于保存所有频道的订阅关系
 
dict *pubsub_channels
 
}

在这个字典中,key 代表的是频道名称,value 是一个链表,这个链表里面存放的是所有订阅这个频道的客户端。

所以当有客户端执行订阅频道的动作的时候,服务器就会将客户端与被订阅的频道在 pubsub_channels 字典中进行关联。

这个时候有两种情况:

  • 该渠道是首次被订阅:首次被订阅说明在字典中并不存在该渠道的信息,那么程序首先要创建一个对应的 key,并且要赋值一个空链表,然后将对应的客户端加入到链表中。此时链表只有一个元素。

  • 该渠道已经被其他客户端订阅过:这个时候就直接将对应的客户端信息添加到链表的末尾就好了。

比如,如果有一个新的客户端 Client 08 要订阅 run 渠道,那么上图就会变成
在这里插入图片描述

如果 Client 08 要订阅一个新的渠道 new_sport ,那么就会变成

在这里插入图片描述

取消订阅

上面介绍的是单个 Channel 的订阅,相反的如果一个客户端要取消订阅相关 Channel,则无非是找到对应的 Channel 的链表,从中删除对应的客户端,如果该客户端已经是最后一个了,则将对应 Channel 也删除。

模式订阅结构

模式渠道的订阅与单个渠道的订阅类似,不过服务器是将所有模式的订阅关系都保存在服务器状态的pubsub_patterns 属性里面。


struct redisServer{
 
//保存所有模式订阅关系
 
list *pubsub_patterns;
 
}

与订阅单个 Channel 不同的是,pubsub_patterns 属性是一个链表,不是字典。节点的结构如下:


struct pubsubPattern{
 
//订阅模式的客户端
 
redisClient *client;
 
//被订阅的模式
 
robj *pattern;
 
} pubsubPattern;

其实 client 属性是用来存放对应客户端信息,pattern 是用来存放客户端对应的匹配模式。

所以对应上面的 Client-06 模式匹配的结构存储如下
在这里插入图片描述

在pubsub_patterns链表中有一个节点,对应的客户端是 Client-06,对应的匹配模式是run*。

订阅模式

当某个客户端通过命令psubscribe 订阅对应模式的 Channel 时候,服务器会创建一个节点,并将 Client 属性设置为对应的客户端,pattern 属性设置成对应的模式规则,然后添加到链表尾部。

创建新节点;

给节点的属性赋值;

将节点添加到链表的尾部;

退订模式

退订模式的命令是punsubscribe,客户端使用这个命令来退订一个或者多个模式 Channel。服务器接收到该命令后,会遍历pubsub_patterns链表,将匹配到的 client 和 pattern 属性的节点给删掉。这里需要判断 client 属性和 pattern 属性都合法的时候再进行删除。

遍历所有的节点,当匹配到相同 client 属性和 pattern 属性的时候就进行节点删除。

发布消息

当一个客户端执行了publish channelName message 命令的时候,服务器会从pubsub_channels和pubsub_patterns 两个结构中找到符合channelName 的所有 Channel,进行消息的发送。在 pubsub_channels 中只要找到对应的 Channel 的 key 然后向对应的 value 链表中的客户端发送消息。

4.2 队列消息推送

    @OnMessage
    public void onMessage(Session session, String message) throws IOException, ParseException {
        //业务逻辑处理
        //向所有用户广播
        redisUtil.convertSend("topic", message);
    } 

4.3 队列消息接收

/**
     * 队列消息接收方法
     * 2022/2/21
     */
    public void receiveMessage(String message) {
        log.info("[开始消费REDIS消息队列topic数据...]");
        try {
            //业务逻辑处理
            webSocketServer.sendMessageForProject(message);
            log.info("[消费REDIS消息队列topic数据成功.]");
        } catch (Exception e) {
            log.error("[消费REDIS消息队列topic数据失败,失败信息:{}]", e.getMessage());
        }
        latch.countDown();
    }
参考来源:「人苼若只茹初見」
原文链接: https://blog.csdn.net/printf88/article/details/123685995

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/702064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Boot 中的 PropertySource 是什么,原理,如何使用

Spring Boot 中的 PropertySource 是什么&#xff0c;原理&#xff0c;如何使用 介绍 在 Spring Boot 中&#xff0c;PropertySource 是一个非常重要的概念。它允许您在应用程序中定义属性&#xff0c;并将这些属性注入到 Spring 环境中。在本文中&#xff0c;我们将介绍 Spr…

GO富集绘图绘制方法,零基础教程,替换数据直接作图,完成版R语言脚本

速绘 丨 GO富集气泡图 本期分享一个快速绘制GO富集结果图的方法&#xff0c;主要使用R语言tidyverse包&#xff0c;只需导入数据即可一步出图&#xff0c;可以自定义显示的数目、颜色、筛选参数&#xff0c;从此以后绘制GO富集图只需1秒。 前言介绍 下面是一个GO富集分析的结果…

Python 控制 AWG70001

0. 实验准备 泰克 AWG70001 一台电脑 一根网线 使用网线连接 AWG70001 和电脑&#xff0c;并且配置 IP 在同一网段下 1. 环境要求 vxi11 numpy struct matplotlib 没有的库可以使用下面的命令安装 pip install vxi11 pip install numpy pip install struct pip install matp…

Android中构建多视图 RecyclerView的正确打开方式

Android中构建多视图 RecyclerView的正确打开方式 简介 漂亮的UI能极大提高用户留存率&#xff0c;相反糟糕的UI将导致App安装率下降。 UI体验对用户留存率有特别大的影响&#xff0c;较差的体验app我可能用不了2s就要卸载掉。 你需要学习内容如下&#xff1a; 使用单个R…

STM32F407开发板DS18B20应用案例

【1】DS18B20介绍 DS18B20是一种数字温度传感器&#xff0c;由Maxim Integrated公司生产。它采用单总线接口&#xff0c;能够在广泛的温度范围内测量温度&#xff0c;并通过数字方式输出温度值&#xff0c;具有较高的精度和稳定性。 以下是DS18B20温度传感器的主要特点和操作…

拯救者Lenovo Legion Y9000X IAH7 2022款(82TF)原装出厂Windows11系统恢复原厂OEM系统

Lenovo联想拯救者笔记本电脑 Legion Y9000X IAH7 2022款(82TF)出厂状态原装Win11系统&#xff0c;恢复原厂系统 系统自带所有驱动、出厂主题壁纸LOGO、Office办公软件、联想电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘 文件格式&#xff1a;ISO 文件大小&am…

人类最新版去水印+外卖CPS小程序源码+独立后台微擎模块

最新版去水印外卖CPS小程序源码 本版本为目前最新版本 修复上个版本后台用户列表加载失败问题 新增轮播图跳转小程序 新增外卖CPS系统

layui学习

官网&#xff1a;Layui镜像站-经典开源模块化前端 UI 框架(官方文档完整镜像) 下载&#xff1a;可以在首页进行下载 快速入门Layui | 枫桥夜泊 如果不知道样式在哪个模块下&#xff0c;引入总的核心样式文件&#xff1b;如果知道样式在哪个模块下&#xff0c;直接引入module…

线程间通信

1、需求 现在两个线程操作一个初始值为0的变量实现一个线程对变量增加1&#xff0c;一个线程对变量减少1交替&#xff0c;来10轮 2、多线程编程模板中 1&#xff09;判断 2&#xff09;干活 3&#xff09;通知 线程间通信&#xff1a; 1.生产者消费者 2.通知等待唤醒机制 3…

Aski AI: 基于人工智能的在线AI工具平台

【产品介绍】 Aski AI是一个基于人工智能的在线AI工具平台&#xff0c;它可以帮助用户快速、准确、全面地解决各种问题。无论是学习、工作、生活、娱乐还是其他领域&#xff0c;只要输入你的问题&#xff0c;Aski AI就会为你提供最合适的答案。此外还提供AI文章写作&#xff0c…

BHQ1 Mal,BHQ2 Maleimide,BHQ3 Mal,马来酰亚胺修饰的BHQ试剂有哪些特点?

一、BHQ-1 Maleimide 产品描述&#xff1a; BHQ-1 Maleimide黑洞猝灭剂-1(BHQ-1)被归类为暗猝灭剂&#xff0c;该淬灭剂能够将一定距离内荧光基团发出的光全部吸收&#xff0c;实现对荧光信号的淬灭&#xff0c;所以可得到更强的特异性&#xff0c;更优化的信噪比。 中文名&a…

【正点原子STM32连载】 第四十五章 FLASH模拟EEPROM实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

第四十五章 FLASH模拟EEPROM实验 STM32本身没有自带EEPROM&#xff0c;但是STM32具有IAP&#xff08;在应用编程&#xff09;功能&#xff0c;所以我们可以把它的FLASH当成EEPROM来使用。本章&#xff0c;我们将利用STM32内部的FLASH来实现第三十六章实验类似的效果&#xff0…

设计模式第18讲——中介者模式(Mediator)

目录 一、什么是中介者模式 二、角色组成 三、优缺点 四、应用场景 4.1 生活场景 4.2 java场景 五、代码实现 5.0 代码结构 5.1 抽象中介者&#xff08;Mediator&#xff09;——LogisticsCenter 5.2 抽象同事类&#xff08;Colleague&#xff09;——Participant 5…

nvm安装node

使用 Windows 系统的我选择使用其推荐的 nvm-windows 来管理 Node.js 版本。 在安装 nvm-windows 前&#xff0c;如果你的电脑中已经安装了 Node.js&#xff0c;那么可以选择卸载&#xff0c;也可以选择不卸载。因为在安装 nvm-windows 的过程中其会询问你是否需要将已安装的 N…

mysql数据库迁移到kingbase人大金仓

1. 启动数据迁移工具 2. 浏览器打开网址[http://localhost:8080/]进入可视化操作界面&#xff0c;在源数据库添加人大金仓数据库信息&#xff0c;测试成功后保存 3.在目标数据库填写需要同步的mysql数据库&#xff0c;添加对应的mysql数据库信息&#xff0c;测试成功后保存 4.在…

The Company Requires Superficial StudyPHP 打开执行PHP ②

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; PHP MYSQL &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f44…

自制游戏引擎

这是一个玩具 1. 引擎使用流程 SmallEngine是引擎的核心模块,封装渲染功能和场景管理功能等Editor是编辑器,类似unity和ue编辑器,能够动态添加对象和组件Sandbox是游戏播放器,能够运行游戏 2. SmallEngine 参考 https://www.bilibili.com/video/BV1KE41117BD/?spm_id_from333…

ARM实验-ARM主程序调用ARM/C语言子程序

一、实验名称&#xff1a;ARM主程序调用ARM/C语言子程序 二、实验目的&#xff1a; 了解ARM应用程序框架。了解ARM汇编程序函数和C语言程序函数相互调用时&#xff0c;遵循的ATPCS标准&#xff1b;了解和掌握ARM汇编程序调用C语言程序函数的基本方法&#xff1b;了解和掌握AR…

操作系统第4章 文件系统 知识点

UNIX系统不存一些具体的指针了 只存文件名和指向i结点的指针 这个删除和截断有什么区别 目录本来放在外存的&#xff0c;有文件打开表&#xff0c;从外存复制到内存的文件打开表中&#xff0c;用户想继续读的时候&#xff0c;不用再去外存搜索目录 访问文件打开表的索引叫文…

基于“SRP模型+”多技术融合在生态环境脆弱性评价模型构建、时空格局演变分析与RSEI 指数的生态质量评价及拓展应用

近年来&#xff0c;国内外学者在生态系统的敏感性、适应能力和潜在影响等方面开展了大量的生态脆弱性研究&#xff0c;他们普遍将生态脆弱性概念与农牧交错带、喀斯特地区、黄土高原区、流域、城市等相结合&#xff0c;评价不同类型研究区的生态脆弱特征&#xff0c;其研究内容…