项目实战 — 消息队列(7){虚拟主机设计(2)}

news2024/11/25 2:57:01

目录

一、消费消息的规则

二、消费消息的具体实现方法

🍅 1、编写消费者类(ConsumerEnv)

🍅 2、编写Consumer函数式接口(回调函数)

🍅 3、编写ConsumeerManager类

        🎄定义成员变量

        🎄notifyConsume()方法

        🎄添加构造方法       

 🎄 addConsumer()方法

🎄 完善consumeMessage()方法

 🍅 4、完成VirtualHost类编写

        🎄  basicConsume()方法编写

        🎄 编写basicAck类(手动应答)

 三、测试VirtualHost

🍅 1、准备工作和收尾工作

🍅 2、测试交换机的创建和删除

🍅3、测试队列的创建和删除

🍅 4、测试绑定的创建和删除

🍅 5、测试发布消息

🍅6、测试消费消息

🎄 先订阅队列,再发送消息

🎄 先发送消息,再订阅队列

🍅 测试basicAck


一、消费消息的规则

前面主要讲了basicPublish,发布消息这一块,同时写了Router类,实现了bindingKey和routingKey的命名规则和匹配规则,主要就是讲的是生产消息。

那么接下来就实现消费者消费消息。

🎊 推送给消费者消息的基本思路:

        1、broker server管理者哪些消费者

        2、收到了对应的消息,把消息推送给消费者

已知,一个broker server中是包含了很多个队列的:

 

🎊 消费者调用basicConsume,就是订阅某个队列的消息:

        1、消费者是以队列的维度订阅消息

        2、一个队列可以有多个消费者

 

 此处,只需要约定消费者如何消费即可。

这里使用“轮询”的方式消费消息:轮询,举例子,如上图,有123三个消费者,让他们分别轮流消费一条消息,依次轮流来,一次消费一个。

具体实现:

1、定义一个类,描述一个消费者

2、然后给每个队列对象(MSGQueue对象)加上属性,相当于一个List,包含若干个消费者对象。


二、消费消息的具体实现方法

在VirtualHost类中实现一个订阅消息的方法basicConsume()

添加一个队列的订阅者,当队列收到消息以后,就要把消息推送给对应的订阅者。

consumerTag:消费者的身份标识

aotoAck:消息被消费完成后,应答的方式,为true自动应答,为false就手动应答。

Consumer:一个回调函数,也就是一个函数式接口(lambda函数底层实现),这样在后面调用basicConsume的时候,并且传实参的时候,就可以写作lambda样子

🍅 1、编写消费者类(ConsumerEnv)

/*
 * 表示一个消费者
 * */
@Data
public class ConsumerEnv {
    private String consumerTag; //消费者身份标识
    private String queueName;
    private boolean autoAck;
//    通过回调处理收到的消息
    private Consumer consumer;
}

然后再MSGQueue.java类中,进行相应的扩充。

    private List<ConsumerEnv> consumerEnvList = new ArrayList<>();
//    记录取到了第几个消费者,方便实现轮询策略
//    AtomicInteger是一个原子性类型,因为consumerSeq再消费信息的时候会被修改,
//    如果使用int可能造成线程不安全,于是这里就使用AtomicInteger
    public AtomicInteger consumerSeq = new AtomicInteger();
//      添加一个新的订阅者
    public void addConsumerEnv(ConsumerEnv consumerEnv){
        synchronized (this){
            consumerEnvList.add(consumerEnv);
        }
    }
//    挑选一个订阅者,处理当前的消息(轮询)
    public ConsumerEnv chooseConsumer(){
        if (consumerEnvList.size() == 0){
//           该队列没有人订阅
            return null;
        }
//        计算当前要取的元素的下标
        int index = consumerSeq.get() % consumerEnvList.size();
//      getAndIncrement()先获取当前值,再加1。相当于 getAndAdd(1).
        consumerSeq.getAndIncrement();   //进行自增
        return consumerEnvList.get(index);
    }


🍅 2、编写Consumer函数式接口(回调函数)

 创建一个Consumer接口。

/*
* 只是一个函数式接口
* 收到消息之后要处理消息时调用的方法
* */
@FunctionalInterface
public interface Consumer {
//    处理投递
//    每次服务器收到消息之后,调用消息,通过这个方法把消息推送给对应的消费者
    void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException;
}


🍅 3、编写ConsumeerManager类

这个类主要就是用来实现消费者消费消息的核心逻辑。主要有以下几块。

消费消息:就是让线程池,执行对应消费者中的回调函数。在调用回调函数的时候,就把消息的内容通过参数,传进去。消费者在最初订阅消息的时候,就把回调注册给broker server。回调函数的内容时消费者确定的,取决于消费者的业务逻辑。

扫描线程:能够感知到哪个队列里面收到了新的消息,扫描线程会取出该消息,找出对应的消费者,将该内容打包成一个任务,丢给线程池去调用

 

为什么需要线程池?

 一些消费者给出的回调函数,处理起来可能会比较耗时,如果只有一个扫描线程,那么可能会导致处理不及时,导致队列中消息越来越多。所以这里引入的扫描线程就轻量的取消息和获取回调,而线程池就用来执行处理的回调函数。

扫描线程如何明白哪个队列中有了新消息?

引入一个阻塞队列。该队列中的元素是有消息的队列的名字,哪一个队列有消息了,就把队列名放到该阻塞队列中。扫描线程就可以从阻塞队列中获取到新增消息的队列的名字。

如何保证消息不被丢失?

使用消息确认(ACK)。在消息确认就是为了避免,消费者的回调方法在执行过程中出错,导致消息丢失这种情况。

为了保证消息不丢失:

(1)在真正执行回调之前,把该消息放到“待确认集合”中,也就是前面MemoryDataCenter中的queueMessageWaitAckMap集合中;

(2)执行回调

(3)当前消费者采取的是autoAck == true,也就是回调执行完毕不抛异常,就算消费成功;消费成功以后,删除消息(硬盘,内存哈希表,待确认集合)

(4)当前消息采取的是autoAck == false,手动应答。也就是消费者这边,在回调方法内部,显示调用basicAck这个核心API。

        🎄定义成员变量

也就是上面提到过的,阻塞队列,扫描线程,线程池。

public class ConsumerManager {
//    持有上层VirtualHost
    private VirtualHost parent;
//    指定一个线程池,负责去执行具体的回调任务
    private ExecutorService workerPool = Executors.newFixedThreadPool(4);
//      引入一个阻塞队列,存放队列名的
    private BlockingQueue<String > tokenQueue = new LinkedBlockingDeque<>();
//   扫描线程
    private Thread scannerThread = null;

}

        🎄notifyConsume()方法

这个方法主要就是为了通知什么时候消费,这里主要就是在发送消息的时候,通知消费,将含有该消息的队列名放在阻塞队列中:

//    通知消费
//    调用时机:发送消息的时候,就调用(sendMessage)
    public void notifyConsume(String queueName) throws InterruptedException {
        tokenQueue.put(queueName);
    }

所以,我们就需要在前面VirtualHost类中的sendMessage方法中再调用一个通知消费的方法:

异常大家自己向上抛一下。

//        通知消费者进行消费
        consumerManager.notifyConsume(queue.getName());

        🎄添加构造方法       

添加构造方法,构造一个线程,编写从队列中取出消息的过程,

其中的consumeMessage(queue)是消费消息的具体实现方法,先列在这里,不实现

 public ConsumerManager(VirtualHost p){
        parent = p;

        scannerThread = new Thread(()->{
//            持续运行
            while (true){
                try {
//                    1、从阻塞队列中拿到队列名
                   String queueName = tokenQueue.take();
//                   2、根据队列名找到队列
                    MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);
                    if (queue == null){
                        throw new MqException("[ConsumerManager]取出令牌后发现,该队列名不存在!queuName = " + queueName);
                    }
//                    3、从队列中消费一个消息
                    synchronized (queue){
                        consumeMessage(queue);
                    }
                } catch (InterruptedException  | MqException e) {
                    e.printStackTrace();
                }
            }
        });
//        把线程设为后台线程
        scannerThread.setDaemon(true);
        scannerThread.start();
    }


private void consumeMessage(MSGQueue queue) {
    //TODO
}

 🎄 addConsumer()方法

该方法主要是为了新增一个Consumer对象到指定的队列中。

//    新增一个Consumer对象到指定的队列中
    public void addConsumer(String consumerTag, String queueName, boolean autoAck, Consumer consumer) throws MqException {
//        找到对应的队列
      MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);
      if (queue == null){
          throw new MqException("[ConsumerManager]队列不存在!queueName = " + queueName);
      }
        ConsumerEnv consumerEnv = new ConsumerEnv(consumerTag,queueName,autoAck,consumer);
        synchronized (queue){
            queue.addConsumerEnv(consumerEnv);
//            如果当前队列中已经有了一些消息,需要立即消费掉
            int n = parent.getMemoryDataCenter().getMessageCount(queueName);
            for (int i = 0; i < n; i++) {
//                调用一次就消费一条消息
                consumeMessage(queue);
            }
        }
    }

🎄 完善consumeMessage()方法

这个方法前面只列了一下,没有实现,这里具体实现一下。

主要有以下几步:

        (1)按照轮询的方式,找出一个消费者

        (2)从队列中取出一个消息 

        (3)把消息丢给回调函数,给线程池处理。

              a. 把消息放到待确认集合中

              b. 真正的执行回调操作

              c. 如果是自动应答,直接删除消息;手动应答,先不处理,交给后续消费者调用                        basicAck()。

private void consumeMessage(MSGQueue queue) {
//        1、按照轮询的方式,找出一个消费者来
        ConsumerEnv luckyDog = queue.chooseConsumer();
        if (luckyDog == null){
//            当前没有消费者,暂时不消费
            return;
        }
//        2、从队列中取出一个消息
//        pollMessage是为了从队列中取出消息
        Message message = parent.getMemoryDataCenter().pollMessage(queue.getName());
        if (message == null) {
//            当前队列没有消息
                    return;
        }
//        3、把消息丢给回调函数中,给线程池处理
        workerPool.submit(() -> {
            try {
//                1、把消息放到待确认集合中
                parent.getMemoryDataCenter().addMessageWaitAck(queue.getName(),message);
//            2、真正执行回调操作
                luckyDog.getConsumer().handleDelivery(luckyDog.getConsumerTag(),message.getBasicProperties(),message.getBody());
//            3、如果当前是自动应答,就可以直接删除消息
//            如果是手动应答,就需要调用basicAck()
                if (luckyDog.isAutoAck()){
//                1).删除硬盘,先看是不是持久化消息
                    if (message.getDeliverMode() == 2){
                        parent.getDiskDataCenter().deleteMessage(queue,message);
                    }
//                    2)、待确认集合中的消息
                    parent.getMemoryDataCenter().removeMessageWaitAck(queue.getName(),message.getMessageId());
//                    3)、删除内存中消息中心的消息
                    parent.getMemoryDataCenter().removeMessage(message.getMessageId());
                    System.out.println("[ConsumerManager]消息被成功消费!queueName = " + queue.getName());
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }


 🍅 4、完成VirtualHost类编写

        🎄  basicConsume()方法编写

该方法主要作用是订阅消息(消费消息)。在VirtualHost中实现。其中调用了ConsumerManager中的方法。

首先在VirtualHost添加consumerManager的实例。

    private ConsumerManager consumerManager = new ConsumerManager(this);

然后写订阅消的方法。 

//    订阅消息
    public boolean basicConsume(String consumerTag, String queueName, boolean autoAck, Consumer consumer){
//        构造一个ConsumerEnv对象,也就是消费者对象,把对应的队列找到,然后将Consumer对象添加到该队列中。
        queueName = virtualHostName + queueName;
        try {
            consumerManager.addConsumer(consumerTag,queueName,autoAck,consumer);
            System.out.println("[VirtualHost]basicConsume成功! queueName = " + queueName);
            return true;
        } catch (Exception e){
            System.out.println("[VirtualHost]basicConsume失败! queueName = " + queueName);
            e.printStackTrace();
            return false;
        }
    }

        🎄 编写basicAck类(手动应答)

public boolean basicAck(String queueName,String messageId){
        queueName = virtualHostName + queueName;
        try{
//            1、获取到消息和队列
            Message message = memoryDataCenter.getMessage(messageId);
            if (message == null){
                throw new MqException("[VirtualHost] 消息不存在!messgeId = " + messageId);
            }
            MSGQueue queue = memoryDataCenter.getQueue(queueName);
            if (queue == null){
                throw new MqException("[VirtualHost] 要确认的队列不存在!queueName = " + queueName);
            }
//            2、删除硬盘上的数据
            if (message.getDeliverMode() == 2){
                diskDataCenter.deleteMessage(queue,message);
            }
//            3、、删除内存中的数据
            memoryDataCenter.removeMessage(messageId);
//            4、删除待确认集合中的数据
            memoryDataCenter.removeMessageWaitAck(queueName,messageId);
            System.out.println("[VirtualHost]basicAck成功!消息被成功确认!queueName = " + queueName);
            return true;
//
        }catch (Exception e){
            System.out.println("[VirtualHost]basicAck失败!消息被成功失败!queueName = " + queueName);
            e.printStackTrace();
            return false;
        }
    }

到这里,我们的虚拟主机VirtualHost类,就算全部写完了。


 三、测试VirtualHost

🍅 1、准备工作和收尾工作

@SpringBootTest
public class VirtualHostTests {
    private VirtualHost virtualHost = null;

    @BeforeEach
    public void setUp(){
        TigerMqApplication.context = SpringApplication.run(TigerMqApplication.class);
        virtualHost = new VirtualHost("default");
    }

    public void tearDown() throws IOException {
        TigerMqApplication.context.close();
        virtualHost = null;
//        把硬盘目录删除
        File dataDir = new File("./data");
        FileUtils.deleteDirectory(dataDir);
    }
}

🍅 2、测试交换机的创建和删除

//    测试创建和删除交换机
    @Test
    public void testExchange(){
        boolean ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,
                true);
        Assertions.assertTrue(ok);
        ok = virtualHost.exchangeDelete("testExchange");
        Assertions.assertTrue(ok);
    }

🍅3、测试队列的创建和删除

//测试创建队列和删除队列
    @Test
    public void testQueue(){
        boolean ok = virtualHost.queueDeclare("testQueue", true);
        Assertions.assertTrue(ok);
        ok = virtualHost.queueDelete("testQueue");
        Assertions.assertTrue(ok);
    }

🍅 4、测试绑定的创建和删除

//    测试创建绑定和删除绑定
    @Test
    public void testQueueBind(){
        boolean ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,
                true);
        Assertions.assertTrue(ok);
        ok = virtualHost.queueDeclare("testQueue", true);
        Assertions.assertTrue(ok);

        ok = virtualHost.queueBind("testQueue","testExchange","testBindingKey");
        Assertions.assertTrue(ok);

        ok = virtualHost.queueUnbind("testQueue","testExchange");
        Assertions.assertTrue(ok);
    }

🍅 5、测试发布消息

//    测试发布消息
@Test
public void testBasicPublish() {
    boolean ok = virtualHost.queueDeclare("testQueue", true);
    Assertions.assertTrue(ok);

    ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,
            true);
    Assertions.assertTrue(ok);

    ok = virtualHost.basicPublish("testExchange", "testQueue", null,
            "hello".getBytes());
    Assertions.assertTrue(ok);
}

🍅6、测试消费消息

🎄 先订阅队列,再发送消息

    //    消费消息
    // 先订阅队列, 后发送消息
    @Test
    public void testBasicConsume1() throws InterruptedException {
        boolean ok = virtualHost.queueDeclare("testQueue", true);
        Assertions.assertTrue(ok);
        ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,
                true);
        Assertions.assertTrue(ok);

        // 先订阅队列
        ok = virtualHost.basicConsume("testConsumerTag", "testQueue", true, new Consumer() {
            @Override
            public void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) {
                // 消费者自身设定的回调方法.
                System.out.println("messageId=" + basicProperties.getMessageId());
                System.out.println("body=" + new String(body, 0, body.length));

                Assertions.assertEquals("testQueue", basicProperties.getRoutingKey());
                Assertions.assertEquals(1, basicProperties.getDeliverMode());
                Assertions.assertArrayEquals("hello".getBytes(), body);
            }
        });
        Assertions.assertTrue(ok);

        Thread.sleep(500);

        // 再发送消息
        ok = virtualHost.basicPublish("testExchange", "testQueue", null,
                "hello".getBytes());
        Assertions.assertTrue(ok);
    }

打印的日志如下:

[DataBaseManger]创建表完成
[DataBaseManger]创建初始数据已经完成
[DataBaseManger]数据库初始化完成
[MemoryDataCenter]队列删除成功!queueName = defaulttestQueue
[VirtualHost]队列创建成功!queueName = defaulttestQueue
[MemoryDataCenter]新交换机添加成功!exchangeName = defaulttestExchange
[VirtualHost] 交换机创建完成!exchangeName = defaulttestExchange
[VirtualHost]basicConsume成功! queueName = defaulttestQueue
[MemoryDataCenter]新消息添加成功!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息被投递到到队列中! messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息从队列中取出!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息进入待确认队列!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
messageId=M-a500879e-5461-4550-8d56-5bef00571ab3
body=hello
[MemoryDataCenter]消息从待确认队列删除!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[MemoryDataCenter]消息被移除!messageId = M-a500879e-5461-4550-8d56-5bef00571ab3
[ConsumerManager]消费被成功消费!queueName = defaulttestQueue

🎄 先发送消息,再订阅队列

 @Test
    public void testBasicConsume2() throws InterruptedException {
        boolean ok = virtualHost.queueDeclare("testQueue", true);
        Assertions.assertTrue(ok);
        ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,
                true);
        Assertions.assertTrue(ok);

        // 先发送消息
        ok = virtualHost.basicPublish("testExchange", "testQueue", null,
                "hello".getBytes());
        Assertions.assertTrue(ok);

        Thread.sleep(500);
        // 再订阅队列
        ok = virtualHost.basicConsume("testConsumerTag", "testQueue", true, new Consumer() {
            @Override
            public void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) {
                // 消费者自身设定的回调方法.
                System.out.println("messageId=" + basicProperties.getMessageId());
                System.out.println("body=" + new String(body, 0, body.length));

                Assertions.assertEquals("testQueue", basicProperties.getRoutingKey());
                Assertions.assertEquals(1, basicProperties.getDeliverMode());
                Assertions.assertArrayEquals("hello".getBytes(), body);
            }
        });
        Assertions.assertTrue(ok);

    }
[MessageFileManager]恢复Message数据完成
[VirtualHost]队列已经存在!queueName = defaulttestQueue
[VirtualHost]交换机已经存在!exchangeName = defaulttestExchange
[MemoryDataCenter]新消息添加成功!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[MemoryDataCenter]消息被投递到到队列中! messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[MemoryDataCenter]消息从队列中取出!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[VirtualHost]basicConsume成功! queueName = defaulttestQueue
[MemoryDataCenter]消息进入待确认队列!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
messageId=M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
body=hello
[MemoryDataCenter]消息从待确认队列删除!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[MemoryDataCenter]消息被移除!messageId = M-b765df78-6997-4ce2-b87e-a6d37e3ee3c8
[ConsumerManager]消费被成功消费!queueName = defaulttestQueue

🍅 测试basicAck

@Test
    public void testBasicAck() throws InterruptedException {
        boolean ok = virtualHost.queueDeclare("testQueue", true);
        Assertions.assertTrue(ok);
        ok = virtualHost.exchangeDeclare("testExchange", ExchangeType.DIRECT,
                true);
        Assertions.assertTrue(ok);

        // 先发送消息
        ok = virtualHost.basicPublish("testExchange", "testQueue", null,
                "hello".getBytes());
        Assertions.assertTrue(ok);

        // 再订阅队列 [把 autoAck 改成 false]
        ok = virtualHost.basicConsume("testConsumerTag", "testQueue", false, new Consumer() {
            @Override
            public void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) {
                // 消费者自身设定的回调方法.
                System.out.println("messageId=" + basicProperties.getMessageId());
                System.out.println("body=" + new String(body, 0, body.length));

                Assertions.assertEquals("testQueue", basicProperties.getRoutingKey());
                Assertions.assertEquals(1, basicProperties.getDeliverMode());
                Assertions.assertArrayEquals("hello".getBytes(), body);

                // [新增手动调用 basicAck]
                boolean ok = virtualHost.basicAck("testQueue", basicProperties.getMessageId());
                Assertions.assertTrue(ok);
            }
        });
        Assertions.assertTrue(ok);

        Thread.sleep(500);
    }
[DataBaseManger]创建表完成
[DataBaseManger]创建初始数据已经完成
[DataBaseManger]数据库初始化完成
[MemoryDataCenter]队列删除成功!queueName = defaulttestQueue
[VirtualHost]队列创建成功!queueName = defaulttestQueue
[MemoryDataCenter]新交换机添加成功!exchangeName = defaulttestExchange
[VirtualHost] 交换机创建完成!exchangeName = defaulttestExchange
[MemoryDataCenter]新消息添加成功!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[MemoryDataCenter]消息被投递到到队列中! messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[MemoryDataCenter]消息从队列中取出!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[VirtualHost]basicConsume成功! queueName = defaulttestQueue
[MemoryDataCenter]消息进入待确认队列!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
messageId=M-72d857bf-fea8-4cf3-a94b-2c87c5226107
body=hello
[MemoryDataCenter]消息被移除!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[MemoryDataCenter]消息从待确认队列删除!messageId = M-72d857bf-fea8-4cf3-a94b-2c87c5226107
[VirtualHost]basicAck成功!消息被成功确认!queueName = defaulttestQueue

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

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

相关文章

Linux系统下安装配置 Nginx 超详细图文教程

Linux系统下安装配置 Nginx 详细教程介绍 一、下载 Nginx 安装包 打开Nginx官网 &#xff1a;http://nginx.org/en/download.html 然后我们找到一个版本&#xff0c;把鼠标移动到上面&#xff0c;右键 - 复制链接地址 我们使用 wget 命令把Nginx安装包下载到/usr/local/目录中…

微信小程序实现当前页面更新上一个页面

日常项目中需要实现的一个价格脱敏功能&#xff1a;通过点击页面二中的查看完整信息 点击回退按钮实现页面一中的价格显露出来 通过查询了大量资料发现 大多数都是通过调用上一个接口的onload 或者onshow 实现视图更新 经测试后 发现 无法实现 只能更改数据 无法更新视图 实现…

shell脚本条件测试语句,if,case

shell脚本条件测试语句&#xff0c;if&#xff0c;case 一.条件测试1.1test命令1.2文件测试1.2.1文件测试常见选项 1.3数值比较1.4字符串比较1.5逻辑测试 二.if语句2.1单分支结构2.3多分支 三.case语句 一.条件测试 1.1test命令 测试特定的表达式是否成立&#xff0c;当条件成…

InVEST模型使用

第一天&#xff1a; 1. 生态系统服务理论联系实践案例讲解 2. InVEST模型的开发历程、不同版本的差异及对数据需求的讲解 3. InVEST所需数据的要求&#xff08;分辨率、格式、投影系统等&#xff09;、获取及标准化预处理讲解 4. InVEST运行常见问题及处理解决方法讲解 5.…

【C++】数据结构与算法:常用查找算法

&#x1f60f;★,:.☆(&#xffe3;▽&#xffe3;)/$:.★ &#x1f60f; 这篇文章主要介绍常用查找算法。 学其所用&#xff0c;用其所学。——梁启超 欢迎来到我的博客&#xff0c;一起学习&#xff0c;共同进步。 喜欢的朋友可以关注一下&#xff0c;下次更新不迷路&#x1…

【果树农药喷洒机器人】Part6:静态PWM变量喷药实验

文章目录 一、引言二、静态PWM变量喷药实验2.1搭建喷药实验平台2.2变量喷药控制实验 一、引言 为综合评估所设计的果树喷药机器人变量喷药效率和质量&#xff0c;验证系统的控制性能和实际作业的可行性&#xff0c;本章开展果树变量喷药实验。首先&#xff0c;通过静态的PWM变…

Mongodb 分页查询数据重复

&#xff08;学习的本质&#xff0c;不在于记住哪些知识&#xff0c;而在于它触发了你的思考——迈克尔桑德尔&#xff09; mongodb排序的限制 相关链接 最多对32个键进行排序排序一致性 其中排序一致性指的是&#xff0c;如果被排序的字段值是重复的&#xff0c;那么在进行…

不同路径数

希望这篇题解对你有用&#xff0c;麻烦动动手指点个赞或关注&#xff0c;感谢您的关注~ 不清楚蓝桥杯考什么的点点下方&#x1f447; 考点秘籍 想背纯享模版的伙伴们点点下方&#x1f447; 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不能错过的模板大全…

C指针:程序员的神奇箭头,穿越内存的冒险之旅!

目录 &#x1f575;️‍♂️ 引言&#xff1a;指针&#xff0c;那些指向星星的小箭头&#xff01; 一、&#x1f3af; 探索箭头&#xff1a;指针的基础知识 1.1 指针是什么&#xff1f; 1.2 解引用操作符&#xff1a;* 是关键 1.3 指针的比较和运算 1.4 空指针&#xff1a…

Gopeed-全平台开源高速下载器 支持(HTTP、BitTorrent、Magnet)协议

一、软件介绍 Gopeed&#xff08;全称 Go Speed&#xff09;&#xff0c;是一款由GolangFlutter开发的高速下载器&#xff0c;开源、轻量、原生&#xff0c;支持&#xff08;HTTP、BitTorrent、Magnet 等&#xff09;协议下载&#xff0c;并且支持全平台使用&#xff0c;底层使…

淘宝商品详情接口(商品列表,APP详情接口)返回示例说明,PC端和APP端

淘宝商品详情包括以下信息&#xff1a; 1. 商品标题&#xff1a;商品的名称或标题&#xff0c;用于描述商品的特点和功能。 2. 商品价格&#xff1a;商品的销售价格&#xff0c;包括原价和促销价等。 3. 商品图片&#xff1a;展示商品的照片或图像&#xff0c;以便顾客可以更…

Pinia的使用,只需四步轻松上手

Pinia与vuex相比&#xff0c;少了mutation和命名空间&#xff0c;支持ts。更适配vue3. 基本使用&#xff1a; 1.创建一个store文件夹&#xff0c;引用createPinia()方法并暴露出去&#xff08;图一&#xff09; 2.main.ts下引用createPinia并注册use一下&#xff08;图2&#x…

SpringBoot启动报错:java: 无法访问org.springframework.boot.SpringApplication

报错原因&#xff1a;jdk 1.8版本与SpringBoot 3.1.2版本不匹配 解决方案&#xff1a;将SpringBoot版本降到2系列版本(例如2.5.4)。如下图&#xff1a; 修改版本后切记刷新Meavn依赖 然后重新启动即可成功。如下图&#xff1a;

【Matlab】PSO优化(单隐层)BP神经网络

上一篇博客介绍了BP-GA&#xff1a;BP神经网络遗传算法(BP-GA)函数极值寻优——非线性函数求极值&#xff0c;本篇博客将介绍用PSO&#xff08;粒子群优化算法&#xff09;优化BP神经网络。 1.优化思路 BP神经网络的隐藏节点通常由重复的前向传递和反向传播的方式来决定&#…

00 - 环境配置

1. 环境说明 使用git gitee 2. 安装配置 ubuntuVM-8-3-ubuntu:~/wuxiang/git$ git --version git version 2.25.1 ubuntuVM-8-3-ubuntu:~/wuxiang/git$2.1 配置user信息 ubuntuVM-8-3-ubuntu:~/wuxiang/git$ git config --global user.name wuxxxxx ubuntuVM-8-3-ubuntu:~…

【遍历】非递归法 二叉树的前中后序遍历

文章目录 非递归法前序遍历后序遍历中序遍历 递归法DFS 非递归法 通过栈Stack来模拟递归。 前序遍历 LeetCode 144 前序遍历&#xff1a;1 2 3 定义&#xff1a;存放答案的List、栈Stack 将root入栈出栈&#xff1a;node&#xff0c;为null则舍弃将node放入list将node.r…

【Spring】-Spring项目的创建

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【Framework】 主要内容&#xff1a;创建spring项目的步骤&#xff1a;先创建一个maven项目&#xff0c;再在pom.xml中添加spring框架支持&#xff0c;最后写一个启动类。 文章目…

程序猿成长之路之密码学篇-分组密码加密模式及IV(偏移量)的详解

Cipher.getInstance("AES/ECB/PKCS5Padding"); Cipher cipher Cipher.getInstance("AES/CBC/PKCS5Padding"); 在进行加解密编程的时候应该有很多小伙伴接触过以上的语句&#xff0c;但是大伙儿在编码过程中是否了解过ECB/CBC的含义、区别以及PKCS5Padding…

学生公寓一进四出智能电表的功能介绍

学生公寓一进四出智能电表石家庄光大远通电气有限公司模块时间控制功能&#xff1a;可设定每个宿舍自动断电和供电的时间&#xff1b;也可以设定某时间段内为小功率输出,设定时间后自动恢复正常供电。权限管理&#xff1a;管理者可对操作人员设定不同操作权限&#xff1b; 软件…

毅哥铡特:修改后的Bellmanford最短路径路由动画演示

修改背景&#xff1a;毅哥铡特自带的《routing_bellmanford.cpp》&#xff0c;按路由跳数进行更新路由表&#xff0c;但是&#xff0c;卫星互联网的卫星路由器节点&#xff0c;可能需要考虑传播传输时延&#xff0c;对应的&#xff0c;可能需要按照两个网络节点的距离来更新路由…