springboot对接rabbitmq并且实现动态创建队列和消费

news2025/1/8 3:47:32

背景

1、对接多个节点上的MQ(如master-MQ,slave-MQ),若读者需要自己模拟出两个MQ,可以部署多个VM然后参考 docker 安装rabbitmq_Steven-Russell的博客-CSDN博客

2、队列名称不是固定的,需要接受外部参数,并且通过模板进行格式化,才能够得到队列名称

3、需要在master-MQ上延迟一段时间,然后将消息再转发给slave-MQ

问题

1、采用springboot的自动注入bean需要事先知道队列的名称,但是队列名称是动态的情况下,无法实现自动注入

2、mq弱依赖,在没有master-mq或者slave-mq时,不能影响到现有能力

解决方案

1、由于mq的队列创建、exchange创建以及队列和exchange的绑定关系是可重入的,所以采用connectFactory进行手动声明

2、增加自定义条件OnMqCondition,防止不必要的bean创建

总体流程

实施过程

搭建springboot项目

参考 搭建最简单的SpringBoot项目_Steven-Russell的博客-CSDN博客

引入amqp依赖

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

引入后续会用到的工具类依赖

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.28</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.40</version>
</dependency>

创建配置文件

在application.yml中增加如下配置

mq:
  master:
    addresses: 192.168.30.128:5672
    username: guest
    password: guest
    vhost: /
  slave:
    addresses: 192.168.30.131:5672
    username: guest
    password: guest
    vhost: /

创建自定义Condition注解和注解实现

package com.wd.config.condition;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnMqCondition.class)
public @interface MqConditional {

    String[] keys();

}
package com.wd.config.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.lang.NonNull;
import org.springframework.util.ObjectUtils;

import java.util.Map;

public class OnMqCondition implements Condition {

    @Override
    public boolean matches(@NonNull ConditionContext context, @NonNull AnnotatedTypeMetadata metadata) {
        Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(MqConditional.class.getName());
        if (annotationAttributes == null || annotationAttributes.isEmpty()) {
            // 为空则不进行校验了
            return true;
        }
        String[] keys = (String[])annotationAttributes.get("keys");
        for (String key : keys) {
            String property = context.getEnvironment().getProperty(key);
            if (ObjectUtils.isEmpty(property)) {
                return false;
            }
        }

        return true;
    }
}

创建多个链接工厂connectFactory

package com.wd.config;

import com.wd.config.condition.MqConditional;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class MqConnectionFactory {

    @Value("${mq.master.addresses}")
    private String masterAddresses;

    @Value("${mq.master.username}")
    private String masterUsername;

    @Value("${mq.master.password}")
    private String masterPassword;

    @Value("${mq.master.vhost}")
    private String masterVhost;

    @Value("${mq.slave.addresses}")
    private String slaveAddresses;

    @Value("${mq.slave.username}")
    private String slaveUsername;

    @Value("${mq.slave.password}")
    private String slavePassword;

    @Value("${mq.slave.vhost}")
    private String slaveVhost;

    @Bean
    @Primary
    @MqConditional(keys = {"mq.master.addresses", "mq.master.vhost", "mq.master.username", "mq.master.password"})
    public ConnectionFactory masterConnectionFactory() {
        return doCreateConnectionFactory(masterAddresses, masterUsername, masterPassword, masterVhost);
    }

    @Bean
    @MqConditional(keys = {"mq.slave.addresses", "mq.slave.vhost", "mq.slave.username", "mq.slave.password"})
    public ConnectionFactory slaveConnectionFactory() {
        return doCreateConnectionFactory(slaveAddresses, slaveUsername, slavePassword, slaveVhost);
    }

    private ConnectionFactory doCreateConnectionFactory(String addresses,
                                                        String username,
                                                        String password,
                                                        String vhost) {
        CachingConnectionFactory cachingConnectionFactory = new CachingConnectionFactory();
        cachingConnectionFactory.setAddresses(addresses);
        cachingConnectionFactory.setUsername(username);
        cachingConnectionFactory.setPassword(password);
        cachingConnectionFactory.setVirtualHost(vhost);
        return cachingConnectionFactory;
    }

}

创建交换机名称枚举 DeclareQueueExchange

package com.wd.config;

public enum DeclareQueueExchange {
    EXCHANGE("exchange"),

    DEAD_EXCHANGE("deadExchange"),

    DELAY_EXCHANGE("delayExchange");

    private final String exchangeName;

    DeclareQueueExchange(String exchangeName) {
        this.exchangeName = exchangeName;
    }

    public String getExchangeName() {
        return exchangeName;
    }
}

创建消息队列模板枚举 DeclareQueueName

package com.wd.config;

public enum DeclareQueueName {
    DELAY_QUEUE_NAME_SUFFIX("_delay"),

    DEAD_QUEUE_NAME_SUFFIX("_dead"),

    QUEUE_NAME_TEMPLATE("wd.simple.queue.{0}");

    private final String queueName;

    DeclareQueueName(String queueName) {
        this.queueName = queueName;
    }

    public String getQueueName() {
        return queueName;
    }
}

创建消息VO和消息

package com.wd.controller.vo;

import com.wd.pojo.Phone;
import lombok.Data;

@Data
public class DelayMsgVo {
    private String queueId;

    private Phone phone;
}
package com.wd.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Phone implements Serializable {
    private static final long serialVersionUID = -1L;

    private String id;

    private String name;

    private Date createTime;

    private List<User> userList;

}
package com.wd.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private static final long serialVersionUID = -1L;

    private String username;

    private Date create;
}

定义队列id列表缓存,用于替换三方缓存,用于队列名称模板初始化

package com.wd.config;

import java.util.ArrayList;
import java.util.List;

public interface QueueIdListConfig {

    /**
     * 先用本地缓存维护队列id
     */
    List<Integer> QUEUE_ID_LIST = new ArrayList<Integer>() {{
        add(111);
        add(222);
        add(333);
    }};
}

创建消息接受入口 controller

注意:此处就以web用户输入为入口,所以创建controller

package com.wd.controller;

import com.alibaba.fastjson2.JSONObject;
import com.rabbitmq.client.*;
import com.wd.config.DeclareQueueExchange;
import com.wd.config.DeclareQueueName;
import com.wd.controller.vo.DelayMsgVo;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;

@RestController
@ConditionalOnBean(value = ConnectionFactory.class, name = "masterConnectionFactory")
public class DynamicCreateQueueController {

    private final ConnectionFactory masterConnectionFactory;

    public DynamicCreateQueueController(@Qualifier(value = "masterConnectionFactory") ConnectionFactory masterConnectionFactory) {
        this.masterConnectionFactory = masterConnectionFactory;
    }

    @PostMapping(value = "sendDelayMsg")
    public String sendMsg2DelayQueue(@RequestBody DelayMsgVo delayMsgVo) throws IOException, TimeoutException {
        doSendMsg2DelayQueue(delayMsgVo);
        return "success";
    }

    private void doSendMsg2DelayQueue(DelayMsgVo delayMsgVo) throws IOException, TimeoutException {
        // 根据id 动态生成队列名称
        String queueNameTemplate = DeclareQueueName.QUEUE_NAME_TEMPLATE.getQueueName();
        String queueName = MessageFormat.format(queueNameTemplate, delayMsgVo.getQueueId());
        String delayQueueName = queueName + DeclareQueueName.DELAY_QUEUE_NAME_SUFFIX.getQueueName();
        String deadQueueName = queueName + DeclareQueueName.DEAD_QUEUE_NAME_SUFFIX.getQueueName();
        // 注意:下述声明交换机和队列的操作是可以重入的,MQ并不会报错
        try (Connection connection = masterConnectionFactory.createConnection();
             Channel channel = connection.createChannel(false)){
            // 声明死信交换机
            channel.exchangeDeclare(DeclareQueueExchange.DEAD_EXCHANGE.getExchangeName(), BuiltinExchangeType.DIRECT);
            // 声明死信队列
            AMQP.Queue.DeclareOk deadQueueDeclareOk = channel.queueDeclare(deadQueueName,
                    true, false, false, null);
            // 定时任务 绑定消费者,避免出现多个消费者以及重启后无法消费存量消息的问题
            //  注意:因为需要保证消费顺序,所以此处仅声明一个消费者
            // 死信队列和交换机绑定
            channel.queueBind(deadQueueName, DeclareQueueExchange.DEAD_EXCHANGE.getExchangeName(), deadQueueName);

            // 声明延迟队列
            Map<String, Object> args = new HashMap<>();
            //设置延迟队列绑定的死信交换机
            args.put("x-dead-letter-exchange", DeclareQueueExchange.DEAD_EXCHANGE.getExchangeName());
            //设置延迟队列绑定的死信路由键
            args.put("x-dead-letter-routing-key", deadQueueName);
            //设置延迟队列的 TTL 消息存活时间
            args.put("x-message-ttl", 10 * 1000);
            channel.queueDeclare(delayQueueName, true, false, false, args);
            channel.exchangeDeclare(DeclareQueueExchange.DELAY_EXCHANGE.getExchangeName(), BuiltinExchangeType.DIRECT);
            channel.queueBind(delayQueueName, DeclareQueueExchange.DELAY_EXCHANGE.getExchangeName(), delayQueueName);

            // 发送消息到延迟队列
            channel.basicPublish(DeclareQueueExchange.DELAY_EXCHANGE.getExchangeName(), delayQueueName, null,
                    JSONObject.toJSONString(delayMsgVo.getPhone()).getBytes(StandardCharsets.UTF_8));
        }

    }

}

创建master延迟消息消费者

package com.wd.mq.consumer;

import com.rabbitmq.client.*;
import com.wd.config.DeclareQueueExchange;
import com.wd.config.DeclareQueueName;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

/**
 * 死信消费者,消费消息转发给targetConnectionFactory对应的目标MQ
 */
public class MasterDeadQueueConsumer extends DefaultConsumer {

    private final ConnectionFactory targetConnectionFactory;

    public MasterDeadQueueConsumer(Channel channel, ConnectionFactory targetConnectionFactory) {
        super(channel);
        this.targetConnectionFactory = targetConnectionFactory;
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 从死信队列的名称中截取队列名称,作为后续队列的名称
        String routingKey = envelope.getRoutingKey();
        String targetQueueName = routingKey.substring(0, routingKey.length() - DeclareQueueName.DEAD_QUEUE_NAME_SUFFIX.getQueueName().length());
        try (Connection targetConnection = targetConnectionFactory.createConnection();
             Channel targetChannel = targetConnection.createChannel(false)){
            // 声明交换机和队列
            targetChannel.exchangeDeclare(DeclareQueueExchange.EXCHANGE.getExchangeName(), BuiltinExchangeType.DIRECT);
            targetChannel.queueDeclare(targetQueueName, true, false, false, null);
            targetChannel.queueBind(targetQueueName, DeclareQueueExchange.EXCHANGE.getExchangeName(), targetQueueName);
            // 转发消息
            targetChannel.basicPublish(DeclareQueueExchange.EXCHANGE.getExchangeName(), targetQueueName, properties, body);
        } catch (TimeoutException e) {
            e.printStackTrace();
            // 注意此处获取的源队列的channel
            getChannel().basicNack(envelope.getDeliveryTag(), false, true);
        }
        // 注意此处获取的源队列的channel
        getChannel().basicAck(envelope.getDeliveryTag(), false);
    }
}

创建slave队列消息消费者

package com.wd.mq.consumer;

import com.alibaba.fastjson2.JSONObject;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.wd.pojo.Phone;

import java.io.IOException;

public class SlaveQueueConsumer extends DefaultConsumer {


    public SlaveQueueConsumer(Channel channel) {
        super(channel);
    }

    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        Phone phone = JSONObject.parseObject(new String(body), Phone.class);
        System.out.println("SlaveQueueConsumer consume ==> " + phone);
        getChannel().basicAck(envelope.getDeliveryTag(), false);
    }
}

创建定时任务,消费延迟消息

注意:因为采用的是死信队列的方式实现的延迟效果,此处只需要消费对应的死信队列即可

package com.wd.mq.quartz;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.wd.config.DeclareQueueExchange;
import com.wd.config.DeclareQueueName;
import com.wd.config.QueueIdListConfig;
import com.wd.mq.consumer.MasterDeadQueueConsumer;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.concurrent.TimeoutException;

@Configuration
@ConditionalOnBean(value = ConnectionFactory.class, name = {"slaveConnectionFactory", "masterConnectionFactory"})
public class MasterDeadQueueSubscribeProcessor {

    private final ConnectionFactory masterConnectionFactory;

    private final ConnectionFactory slaveConnectionFactory;

    public MasterDeadQueueSubscribeProcessor(@Qualifier(value = "masterConnectionFactory") ConnectionFactory masterConnectionFactory,
                                             @Qualifier(value = "slaveConnectionFactory") ConnectionFactory slaveConnectionFactory) {
        this.masterConnectionFactory = masterConnectionFactory;
        this.slaveConnectionFactory = slaveConnectionFactory;
    }

    /**
     * 消费死信队列信息,并且转发到其他mq
     */
    @Scheduled(fixedDelay = 10 * 1000)
    public void subscribeMasterDeadQueue() throws IOException, TimeoutException {
        // 根据id 动态生成队列名称
        // 此处的queueIdList可以从第三方缓存查询得到,并且和sendDelayMsg接口保持同步刷新,此处先用本地缓存代替,id同步刷新机制不是重点,此处暂不讨论
        for (Integer id : QueueIdListConfig.QUEUE_ID_LIST) {
            String queueNameTemplate = DeclareQueueName.QUEUE_NAME_TEMPLATE.getQueueName();
            String deadQueueName = MessageFormat.format(queueNameTemplate, id) + DeclareQueueName.DEAD_QUEUE_NAME_SUFFIX.getQueueName();

            try (Connection connection = masterConnectionFactory.createConnection();
                 Channel channel = connection.createChannel(false)){
                AMQP.Queue.DeclareOk queueDeclare = channel.queueDeclare(deadQueueName, true, false, false, null);
                if (queueDeclare.getConsumerCount() == 0) {
                    channel.exchangeDeclare(DeclareQueueExchange.DEAD_EXCHANGE.getExchangeName(), BuiltinExchangeType.DIRECT);
                }
                channel.queueBind(deadQueueName, DeclareQueueExchange.DEAD_EXCHANGE.getExchangeName(), deadQueueName);
                channel.basicConsume(deadQueueName, false, new MasterDeadQueueConsumer(channel, slaveConnectionFactory));
            }
        }
    }

}

创建定时任务,消费slave队列的消息

package com.wd.mq.quartz;


import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.wd.config.DeclareQueueExchange;
import com.wd.config.DeclareQueueName;
import com.wd.config.QueueIdListConfig;
import com.wd.mq.consumer.SlaveQueueConsumer;
import org.springframework.amqp.rabbit.connection.Connection;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;

import java.io.IOException;
import java.text.MessageFormat;
import java.util.concurrent.TimeoutException;

@Configuration
@ConditionalOnBean(value = ConnectionFactory.class, name = "slaveConnectionFactory")
public class SlaveQueueSubscribeProcessor {

    private final ConnectionFactory slaveConnectionFactory;

    public SlaveQueueSubscribeProcessor(@Qualifier(value = "slaveConnectionFactory") ConnectionFactory slaveConnectionFactory) {
        this.slaveConnectionFactory = slaveConnectionFactory;
    }

    /**
     * 消费队列信息
     */
    @Scheduled(fixedDelay = 10 * 1000)
    public void subscribeSlaveDeadQueue() throws IOException, TimeoutException {
        // 根据id 动态生成队列名称
        // 此处的queueIdList可以从第三方缓存查询得到,并且和sendDelayMsg接口保持同步刷新,此处先用本地缓存代替
        for (Integer id : QueueIdListConfig.QUEUE_ID_LIST) {
            String queueNameTemplate = DeclareQueueName.QUEUE_NAME_TEMPLATE.getQueueName();
            String queueName = MessageFormat.format(queueNameTemplate, id);
            try (Connection connection = slaveConnectionFactory.createConnection();
                 Channel channel = connection.createChannel(false)){
                AMQP.Queue.DeclareOk queueDeclare = channel.queueDeclare(queueName, true, false, false, null);
                if (queueDeclare.getConsumerCount() == 0) {
                    channel.basicConsume(queueName, false, new SlaveQueueConsumer(channel));
                }
                channel.exchangeDeclare(DeclareQueueExchange.EXCHANGE.getExchangeName(), BuiltinExchangeType.DIRECT);
                channel.queueBind(queueName, DeclareQueueExchange.EXCHANGE.getExchangeName(), queueName);
            }
        }

    }

}

启动项目

请求接口发送消息 http://localhost:8080/sendDelayMsg

检查消息传递过程

先在master-mq延迟队列发现消息

再到master-mq死信队列中发现消息

再到slave-mq中发现消息

检查日志打印

发现SlaveQueueConsumer打印如下日志:

结论

消息传递流程如下,验证通过

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

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

相关文章

【LRU】一文让你弄清 Redis LRU 页面置换算法

Q&#xff1a;一天同事问&#xff0c;我放在 redis 中的 key&#xff0c;为什么有时候过一段时间数据就没有了&#xff0c;我并没有设置过期时间呀&#xff1f;&#xff1f;&#x1f633;&#x1f633; A&#xff1a;你的 redis 淘汰策略是什么样的&#xff0c;这个 key 可能是…

dirname - return directory part of PATH.

用Visual Studio 2022开发Linux程序, 用ssh连接 函数单元测试 下载glibc解压到E:\library\GNU\glibc-2.38 mzhDESKTOP-GITL67P:~$ sudo /etc/init.d/ssh start * Starting OpenBSD Secure Shell server sshd …

【Spring】-Bean的作用域和生命周期

作者&#xff1a;学Java的冬瓜 博客主页&#xff1a;☀冬瓜的主页&#x1f319; 专栏&#xff1a;【Framework】 主要内容&#xff1a;Lombok的使用&#xff0c;Bean作用域的分类和修改。Singleton、Prototype。spring的执行流程&#xff0c;Bean的生命历程。 文章目录 一、Bea…

树莓派+墨水屏 = DIY一个超慢速电影播放器

安装电子墨水屏这里使用了 Waveshare 的一款墨水屏&#xff0c;带驱动板。将驱动板插入树莓派的 GPIO 即完成屏幕和树莓派的连接。驱动这个屏幕需要启用树莓派的 SPI 接口。运行 sudo raspi-config 进入配置工具来启用 SPI 运行python例程 安装函数库 sudo apt-get update su…

指针笔试题详解

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂。 目录 1.前言 2.指针题写出下列程序的结…

Unity Windows上Inspector界面黑屏无法显示

问题描述&#xff1a;在Windows上Unity 打开工程Inspector显示黑色&#xff0c;不可用。如下图&#xff1a; 可能的问题。 Unity 设置Windows上默认渲染方式显卡不支持。 解决方案&#xff1a; 一、换一个好一点显卡 二、如下图&#xff1a;

Transformer的上下文学习能力

《Uncovering mesa-optimization algorithms in Transformers》 论文链接&#xff1a;https://arxiv.org/abs/2309.05858 为什么 transformer 性能这么好&#xff1f;它给众多大语言模型带来的上下文学习 (In-Context Learning) 能力是从何而来&#xff1f;在人工智能领域里&…

实验室安全教育与考试

目录 我的错题&#xff08;2个&#xff09;新知识题目&#xff08;10个&#xff09;刚开始不太理解的题目&#xff08;10个&#xff09;写在最后&#xff08;免责声明&#xff09; 我的错题&#xff08;2个&#xff09; 18.发生电气火灾时可以使用的灭火设备包括&#xff1a;&…

SAP Service服务重注册技术手册

当SAP服务被卸载后,或SAP虚拟机整机copy后(可能还需要涉及主机名更改),需要对SAP服务重注册。 在路径 \sapmnt\<SID>\ DVEBMGS00\exe下使用程序sapstartsrv.exe来卸载、安装SAP服务: 其中<SID>、NR参考Service中需要卸载的服务名(卸载后,Services列表中的SA…

【UE 粒子练习】08——LOD概述

目录 概念 应用举例 一、检查当前粒子系统中是否设置了LOD 二、添加LOD 三、LOD设置&#xff08;单个粒子发射器&#xff09; 四、LOD设置&#xff08;多个粒子发射器&#xff09; 概念 在 Unreal Engine 中&#xff0c;LOD&#xff08;Level of Detail&#xff0c;细…

Kubernetes 学习总结(37)—— Kubernetes 之 CoreDNS 企业级应用

前言 IP 地址会有变更&#xff0c;程序配置IP地址&#xff0c;所有涉及到此IP的地方都需要改变&#xff0c;对运维和研发都不友好。IP不容易被记住。引入域名来替换 IP&#xff0c;这样业务使用唯一标识域名&#xff0c;域名可以通过 DNS 服务器解析成 IP 供业务三层通信使用。…

时间复杂度、空间复杂度

一、时间复杂度 1、概念 时间复杂度&#xff1a;计算的是当一个问题量级增加的时间&#xff0c;时间增长的趋势&#xff1b; O&#xff08;大O表示法&#xff09;&#xff1a;渐进的时间复杂度 2、举例 ① 以下 for 循环的时间复杂度&#xff1a;O(1 3n) O(n) 去掉常数…

rom修改----安卓系列机型如何内置app 如何选择so文件内置

系统内置app的需求 在与各工作室对接中操作单中&#xff0c;很多需要内置客户特定的有些app到系统里&#xff0c;这样方便客户刷入固件后直接调用。例如内置apk 去开机引导 去usb调试 默认开启usb安全设置等等。那么很多app内置有不同的反应。有的可以直接内置。有的需要加so…

(十三)VBA常用基础知识:编程时各种常用操作之设值,取值,活动窗口设定

cell里设置内容的两个写法 Sub test() Range("A1").Value "帅哥" Cells(1, 2).Value "帅哥 too" End Sub2.cell里内容的取得 Sub test() Range("A1").Value "帅哥" Cells(1, 2).Value "帅哥 too" a Range(…

Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置相机的图像剪切(ROI)功能(C#)

Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置相机的图像剪切&#xff08;ROI&#xff09;功能&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机的图像剪切&#xff08;ROI&#xff09;功能的技术背景CameraExplorer如何使用图像剪切&#xff08;ROI&#xff09;功…

[nodejs]NVM使用指南

安装 官网链接 使用 # 版本号 nvm version# 显示node是运行在32位还是64位。 nvm arch# 显示已安装的列表 nvm list nvm ls# 使用制定版本node。可指定32/64位 nvm use [version] [arch]# 显示可安装的所有版本 nvm list available# 安装最新版本 nvm install latest# 安装指…

【Linux】系统编程简单线程池(C++)

目录 【1】线程池概念 【1.1】线程池 【1.2】线程池的应用场景 【1.3】线程池的种类 【1.4】线程池示例 【2】线程池代码 【1】线程池概念 【1.1】线程池 一种线程使用模式。线程过多会带来调度开销&#xff0c;进而影响缓存局部性和整体性能。而线程池维护着多个线程&a…

【新书推荐】用户画像:了解用户,助力企业成长 ——《用户画像:平台构建与业务实践》

文章目录 〇、引子一、什么是用户画像二、用户画像的优势三、如何实现用户画像四、用户画像应用中的问题五、总结新书推荐 —— 《用户画像&#xff1a;平台构建与业务实践》内容简介目录 〇、引子 在当今市场竞争激烈的时代&#xff0c;了解用户需求、提高用户体验已成为企业…

WebGL 雾化

目录 前言 如何实现雾化 线性雾化公式 雾化因子关系图 根据雾化因子计算片元颜色公式 示例程序&#xff08;Fog.js&#xff09; 代码详解​编辑 详解如何计算雾化因子&#xff08;clamp()&#xff09; 详解如何计算最终片元颜色&#xff08;根据雾化因子计算片元颜色…

二、搭建Java环境

搭建Java环境 搭建Java环境1.1.下载JDK1.2.在Win10下配置JDK环境 —————————————————————————————————————————————————— ———————————————————————————————————————————————…