关于 自定义的RabbitMQ的RabbitMessageContainer注解-实现原理
概述
RabbitMessageContainer注解 的主要作用就是 替换掉@Configuration配置类中的各种@Bean配置;
采用注解的方式可以让我们 固化配置,降低代码编写复杂度、减少配置错误情况的发生,提升编码调试的效率、提高业务的可用性。
- 为什么说“降低代码编写的复杂度”呢?因为,用一行注解代替了原本好几十行的代码。
- 为什么说“减少配置错误情况的发生,提升编码调试的效率”呢?因为,开发者从其他@Configuration配置文件复制粘贴的代码,有时会忘记修改某些Bean名称,而启动又不会报错,最终会导致队列没有消费者,需要浪费时间排查问题。
-
为什么说“提高业务的可用性”呢?因为,组件默认配置了死信队列机制,当消费失败的时候,将异常抛出即可重试,避免因为没有配置死信队列而导致消息丢失。(如果继承AbstractJdkSerializeListener/AbstractJsonSerializeListener可以在重试一定次数后将消息落库并且丢弃)
接入方式
该组件使用Spring Boot的自动装配能力,只需要引入pom依赖即可完成接入。
<dependency><groupId>com.ccbscf</groupId><artifactId>ccbscf-biz-enhancer-rabbitmq-starter</artifactId><version>1.0.1-SNAPSHOT</version>
</dependency>
支持哪些能力?
简单来说,以前@Bean注入方式常用的能力,这个组件都支持,以下是具体注解信息及属性配置:
-
com.ccbscf.biz.enhancer.rabbitmq.annotation.RabbitMessageContainer注解
/*** 向spring中注入SimpleMessageListenerContainer容器* 暂时只对Container的acknowledgeMode、exposeListenerChannel、prefetchCount、concurrentConsumers、maxConcurrentConsumers提供了赋值的扩展,如果需要其他的字段赋值,需要升级组件*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RabbitMessageContainer {/*** container的name,向spring容器注入bean* @return*/String value();/*** 定义绑定关系,队列、交换器、路由key的定义都在这里面* 这里为什么是定义数组呢,因为同一个Container是可以绑定多个队列的,因此这里是数组;* @return*/QueueBinding[] bindings();/*** @return* @see AbstractMessageListenerContainer#setAcknowledgeMode(org.springframework.amqp.core.AcknowledgeMode)*/AcknowledgeMode acknowledgeMode() default AcknowledgeMode.MANUAL;/*** @return* @see AbstractMessageListenerContainer#setExposeListenerChannel(boolean)*/boolean exposeListenerChannel() default true;/*** @return* @see SimpleMessageListenerContainer#setPrefetchCount(int)*/int prefetchCount() default 5;/*** @return* @see SimpleMessageListenerContainer#setConcurrentConsumers(int)*/int concurrentConsumers() default 1;/*** @return* @see SimpleMessageListenerContainer#setMaxConcurrentConsumers(int)*/int maxConcurrentConsumers() default 1;/*** 失败 抛出异常 捕捉到异常以后 是否进行重试 默认重试* @return*/boolean needRetry() default true;/*** 自定义的Listener维度的重试次数上限* @return*/int customerRetriesLimitForListener() default -1;/*** 重试时间间隔* @return*/long retryTimeInterval() default -1;
}
上面是@RabbitMessageContainer注解的源代码;原本@Bean中SimpleMessageListenerContainer常用的参数设置,这里都进行了支持,如果有新的个性化字段赋值,可以对组件进行扩展,给注解增加字段,同时注入BeanDefinition的时候赋值即可。
除了实现@Bean方式常用字段,另外增加了以下几个功能字段:
- needRetry:失败 抛出异常 捕捉到异常以后 是否进行重试? 默认重试
- customerRetriesLimitForListener:自定义的Listener维度的重试次数上限,此优先级高于全局的次数上限配置
- retryTimeInterval:重试时间间隔,固定时间间隔,不支持梯度;这个配置是加在队列参数上的,一旦配置生效,就无法修改,这个RabbitMQ的特性
为了理解起来更直观,下面展示出原有的@Bean注入方式的示例:
public static SimpleMessageListenerContainer buildSimpleMessageListenerContainer(Queue queue, ConnectionFactory connectionFactory, Object messageListener) {SimpleMessageListenerContainer simpleMessageListenerContainer = new SimpleMessageListenerContainer(connectionFactory);simpleMessageListenerContainer.setQueues(queue);simpleMessageListenerContainer.setMaxConcurrentConsumers(1);simpleMessageListenerContainer.setConcurrentConsumers(1);simpleMessageListenerContainer.setPrefetchCount(5);simpleMessageListenerContainer.setExposeListenerChannel(true);simpleMessageListenerContainer.setMessageListener(messageListener);simpleMessageListenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL);return simpleMessageListenerContainer;
}
-
com.ccbscf.biz.enhancer.rabbitmq.annotation.QueueBinding注解
@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface QueueBinding {/*** 绑定关系的name,主要用于向容器中注入bean的名称* @return*/String value();/*** @return the queue.*/Queue queue();/*** @return the exchange.*/Exchange exchange();/*** @return the routing key or pattern for the binding.*/String key() default "";
}
上面是@QueueBinding注解的源代码;原本@Bean中Binding常用的参数设置,这里都进行了支持,如果有新的个性化字段赋值,可以对组件进行扩展,给注解增加字段,同时注入BeanDefinition的时候赋值即可。
为了理解起来更直观,下面展示出原有的@Bean注入方式的示例:
@Beanpublic Binding sendSuperviseBinding(TopicExchange approveDocDatumTopicExchange) {return BindingBuilder.bind(sendSuperviseQueue()).to(approveDocDatumTopicExchange).with(DOC_DATUM_TOPIC_APPROVE_ROUTING_KEY);}
-
com.ccbscf.biz.enhancer.rabbitmq.annotation.Queue注解
@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface Queue {/*** @return the queue name or "" for a generated queue name (default).*/String value();/*** @return true if the queue is to be declared as durable.*/boolean durable() default true;/*** @return true if the queue is to be declared as exclusive.*/boolean exclusive() default false;/*** @return true if the queue is to be declared as auto-delete.*/boolean autoDelete() default false;/*** 是否延迟队列* @return*/boolean delayConsumer() default false;/*** delayConsumer为true的情况下该字段才会生效,单位:ms* 如果设置了delayConsumer=true延迟队消费开启,但是未设置delayTime延迟消费时间,默认值是10分钟* @return*/long delayTime() default -1;
}
上面是@Queue注解的源代码;原本@Bean中Queue常用的参数设置,这里都进行了支持,如果有新的个性化字段赋值,可以对组件进行扩展,给注解增加字段,同时注入BeanDefinition的时候赋值即可。
除了实现@Bean方式常用字段,另外增加了以下几个功能字段:
- delayConsumer:是否延迟队列?默认为false,如果需要开启延迟消费的功能,需要配置为true
- delayTime:delayConsumer为true的情况下该字段才会生效,单位:ms;如果设置了delayConsumer=true延迟队消费开启,但是未设置delayTime延迟消费时间,默认值是10分钟
为了理解起来更直观,下面展示出原有的@Bean注入方式的示例:
new Queue(queueName, true, false, false, params)
-
com.ccbscf.biz.enhancer.rabbitmq.annotation.Exchange注解
@Target({})
@Retention(RetentionPolicy.RUNTIME)
public @interface Exchange {/*** @return the exchange name.*/String value();/*** The exchange type - only DIRECT, FANOUT TOPIC, and HEADERS exchanges are supported.* @return the exchange type.*/String type() default ExchangeTypes.TOPIC;/*** @return true if the exchange is to be declared as durable.*/boolean durable() default true;/*** @return true if the exchange is to be declared as auto-delete.*/boolean autoDelete() default false;
}
上面是@Exchange注解的源代码;原本@Bean中Exchange常用的参数设置,这里都进行了支持,如果有新的个性化字段赋值,可以对组件进行扩展,给注解增加字段,同时注入BeanDefinition的时候赋值即可。
为了理解起来更直观,下面展示出原有的@Bean注入方式的示例:
@Beanpublic TopicExchange bizCcbDefaultTopicExchange() {return new TopicExchange(BIZ_CCB_DEFAULT_TOPIC_EXCHANGE, true, false);}
核心代码逻辑
其实,实现思路非常简单,原有方式:通过开发者定义@Bean配置向spring容器中添加BeanDefinition并生成单例Bean;新的方式:根据开发者配置的注解信息集中式的生成BeanDefinition并注册到spring容器即可。
至于绑定关系、队列、交换器向MQ消息中心注册的过程不受任何影响,因为本来@Bean就是在向容器注入bean而已;
核心代码都在这一个RabbitMqEnhancerBeanDefinitionRegistry类,这个类实现了BeanDefinitionRegistryPostProcessor接口,当然BeanDefinitionRegistryPostProcessor也继承了BeanFactoryPostProcessor接口,只不过我们只使用了BeanDefinitionRegistryPostProcessor具有的特性,向容器中注入BeanDefinition信息;至于spring生成单例bean的过程,我们不去干预还是交给spring来自行完成。
从@RabbitMessageContainer、@Queue、@Exchange、@QueueBinding注解中获取信息,创建相应的BeanDefinition并注册到容器中,由spring容器管理,充分利用spring现有机制,自动创建bean实例,尽可能减少硬编码干预spring的流程。
源代码如下:
/*** @ClassName RabbitMqEnhancerBeanDefinitionRegistry* @Description* 处理@RabbitMessageContainer、@Queue、@Exchange、@QueueBinding注解,以及创建相应的BeanDefinition注册到容器中;* 由spring容器管理,充分利用spring现有机制,自动创建bean实例,尽可能减少硬编码干预spring的流程。* 还有一种实现思路是:* 自定义一个BeanPostProcessor的实现类,同时实现BeanFactoryAware接口(目的是获取到BeanFactory,用ApplicationContextAware也行,但是BeanFactoryAware更好些);* 调用postProcessAfterInitialization方法,拦截Listener并识别注解信息,创建并注册BeanDefinition,调用BeanFactory的getBean方法,创建单例bean对象;* 这种方式不仅个性化spring的BeanDefinition的注册,而且还个性化了bean的创建过程,因此不是最优的方式。* @Author zhangyuxuan* @Date 2023/9/13 15:29* @Version 1.0*/
public class RabbitMqEnhancerBeanDefinitionRegistry implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {private Environment environment;/*** 处理@RabbitMessageContainer、@Queue、@Exchange、@QueueBinding注解,以及创建相应的BeanDefinition注册到容器中;* 由spring容器管理,充分利用spring现有机制,自动创建bean实例,尽可能减少硬编码干预spring的流程。** @param registry* @throws BeansException*/@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {for (String beanDefinitionName : registry.getBeanDefinitionNames()) {BeanFactory beanFactory = (BeanFactory) registry;//获取bean对应的ClassClass<?> type = beanFactory.getType(beanDefinitionName);//获取RabbitMessageContainer注解RabbitMessageContainer rabbitMessageContainer = AnnotationUtils.findAnnotation(type, RabbitMessageContainer.class);if (rabbitMessageContainer == null) {continue;}//获取QueueBinding注解QueueBinding[] bindings = rabbitMessageContainer.bindings();if (bindings.length == 0) {continue;}//存储queue信息,都是实际消费消息 绑定Listener的队列List<String> queueNameList = new ArrayList<>();// 这里为什么是定义数组呢,因为同一个Container是可以绑定多个队列的,因此这里是数组;for (QueueBinding binding : bindings) {Queue queue = binding.queue();Exchange exchange = binding.exchange();//是否开启延迟消费功能boolean needDelay = queue.delayConsumer();//是否开启重试功能boolean needRetry = rabbitMessageContainer.needRetry();//死信重试路由keyString retryRoutingKey = obtainDoConsumeQueue(queue, needDelay) + DL_ROUTING_KEY_SUFFIX;//延迟消费 实际消费的交换器String exchangeForDelay = environment.getProperty("spring.application.name", "") + DELAY_EXCHANGE_NAME_SUFFIX;//失败重试 死信交换器String exchangeForDl = environment.getProperty("spring.application.name", "") + DL_EXCHANGE_NAME_SUFFIX;//失败重试 重试交换器String exchangeForRetry = environment.getProperty("spring.application.name", "") + RETRY_EXCHANGE_NAME_SUFFIX;if (needDelay) {//延迟消费String delayRoutingKey = queue.value() + DELAY_CONSUME_ROUTE_SUFFIX;//用于延迟消费//用户定义的原队列BindingWrapper bindingWrapper = BindingWrapper.generateBinding(binding.value(), binding.key()).buildQueue(queue.value(), obtainMapForDelayQueue(delayRoutingKey, exchangeForDelay, queue.delayTime()), queue.durable(), queue.exclusive(), queue.autoDelete()).buildExchange(exchange.value(), exchange.type(), exchange.durable(), exchange.autoDelete());//注册用户定义的原队列相关配置configRabbitMq(registry, bindingWrapper, true);//实际消费消息的队列BindingWrapper bindingWrapperConsume = BindingWrapper.generateBinding(binding.value() + DELAY_CONSUME_BINDING_SUFFIX, delayRoutingKey).buildQueue(obtainDoConsumeQueue(queue, true), obtainMapForConsumeQueue(needRetry, retryRoutingKey, exchangeForDl), queue.durable(), queue.exclusive(), queue.autoDelete()).buildExchange(exchangeForDelay, exchange.type(), exchange.durable(), exchange.autoDelete());//注册实际消费消息的队列相关配置,延迟交换器已经在配置中注册configRabbitMq(registry, bindingWrapperConsume, false);//存储queue信息,都是实际消费消息 绑定Listener的队列queueNameList.add(bindingWrapperConsume.getQueueWrapper().getQueueName());} else {//非延迟消费BindingWrapper bindingWrapper = BindingWrapper.generateBinding(binding.value(), binding.key()).buildQueue(queue.value(), obtainMapForConsumeQueue(needRetry, retryRoutingKey, exchangeForDl), queue.durable(), queue.exclusive(), queue.autoDelete()).buildExchange(exchange.value(), exchange.type(), exchange.durable(), exchange.autoDelete());//用户定义的原队列configRabbitMq(registry, bindingWrapper, true);//存储queue信息,都是实际消费消息 绑定Listener的队列queueNameList.add(bindingWrapper.getQueueWrapper().getQueueName());}if (needRetry) {//是否需要重试//死信队列BindingWrapper bindingWrapperDl = BindingWrapper.generateBinding(binding.value() + DL_BINDING_SUFFIX, retryRoutingKey).buildQueue(queue.value() + DL_QUEUE_SUFFIX, obtainMapForDlQueue(retryRoutingKey, exchangeForRetry, rabbitMessageContainer.retryTimeInterval()), queue.durable(), queue.exclusive(), queue.autoDelete()).buildExchange(exchangeForDl, DIRECT, exchange.durable(), exchange.autoDelete());//注册死信队列相关配置,死信交换器已经在配置中注册configRabbitMq(registry, bindingWrapperDl, false);//重试队列 用于重新消费BindingWrapper bindingWrapperRetry = BindingWrapper.generateBinding(binding.value() + RETRY_BINDING_SUFFIX, retryRoutingKey).buildQueue(obtainDoConsumeQueue(queue, needDelay), Collections.emptyMap(), queue.durable(), queue.exclusive(), queue.autoDelete()).buildExchange(exchangeForRetry, exchange.type(), exchange.durable(), exchange.autoDelete());// 向容器中注册binding的BeanDefinition,队列复用用户定义的,重试交换器已经在配置中创建registryBinding(registry, bindingWrapperRetry);}}// 向容器中注册container的BeanDefinitionregistryContainer(registry, beanDefinitionName, rabbitMessageContainer, queueNameList);}}/*** 因为延迟消费情况的存在,因此需要获取实际消费队列的逻辑* @param queue* @param needDelay* @return*/private String obtainDoConsumeQueue(Queue queue, boolean needDelay) {return needDelay ? queue.value() + DELAY_CONSUME_QUEUE_SUFFIX : queue.value();}/*** 向容器中注册mq的配置,包括queue、exchange、binding* @param registry* @param bindingWrapper*/private void configRabbitMq(BeanDefinitionRegistry registry, BindingWrapper bindingWrapper, boolean isNeedCreateExchange) {// 向容器中注册queue的BeanDefinitionregistryQueue(registry, bindingWrapper);// 向容器中注册exchange的BeanDefinitionif (isNeedCreateExchange) {registryExchangeIfNecessary(registry, bindingWrapper);}// 向容器中注册binding的BeanDefinitionregistryBinding(registry, bindingWrapper);}/*** 向容器中注册container的BeanDefinition* @param registry* @param beanDefinitionName* @param rabbitMessageContainer* @param queueNameList*/private void registryContainer(BeanDefinitionRegistry registry, String beanDefinitionName, RabbitMessageContainer rabbitMessageContainer, List<String> queueNameList) {ManagedArray managedArray = new ManagedArray("org.springframework.amqp.core.Queue", queueNameList.size());for (String queueName : queueNameList) {managedArray.add(new RuntimeBeanReference(queueName));}AbstractBeanDefinition containerBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SimpleMessageListenerContainer.class).addConstructorArgReference("shadowConnectionFactory").addPropertyValue("queues", managedArray).addPropertyReference("messageListener", beanDefinitionName).addPropertyValue("acknowledgeMode", rabbitMessageContainer.acknowledgeMode()).addPropertyValue("maxConcurrentConsumers", rabbitMessageContainer.maxConcurrentConsumers()).addPropertyValue("concurrentConsumers", rabbitMessageContainer.concurrentConsumers()).addPropertyValue("prefetchCount", rabbitMessageContainer.prefetchCount()).addPropertyValue("exposeListenerChannel", rabbitMessageContainer.exposeListenerChannel()).getBeanDefinition();registry.registerBeanDefinition(rabbitMessageContainer.value(), containerBeanDefinition);}/*** 向容器中注册queue的BeanDefinition* @param registry* @param bindingWrapper*/private void registryQueue(BeanDefinitionRegistry registry, BindingWrapper bindingWrapper) {BindingWrapper.QueueWrapper queueWrapper = bindingWrapper.getQueueWrapper();AbstractBeanDefinition queueBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(org.springframework.amqp.core.Queue.class).addConstructorArgValue(queueWrapper.getQueueName()).addConstructorArgValue(queueWrapper.isDurable()).addConstructorArgValue(queueWrapper.isExclusive()).addConstructorArgValue(queueWrapper.isAutoDelete()).addConstructorArgValue(queueWrapper.getParams()).getBeanDefinition();registry.registerBeanDefinition(queueWrapper.getQueueName(), queueBeanDefinition);}/*** 如果有必要,向容器注入交换器* @param registry* @param bindingWrapper*/private void registryExchangeIfNecessary(BeanDefinitionRegistry registry, BindingWrapper bindingWrapper) {// 如果容器中已经被ConfigurationClassPostProcessor添加了同名的Exchange的BeanDefinition,那就不在添加了;// 一是兼容项目原有代码已经通过@Bean方式注入了BeanDefinition;// 二是Exchange本来原则上就是应该尽可能服用的,所以多个Listener一定会存在使用相同的Exchange的情况;if (!registry.containsBeanDefinition(bindingWrapper.getExchangeWrapper().getExchangeName())) {registryExchange(registry, bindingWrapper);}}/*** 向容器中注册exchange的BeanDefinition* @param registry* @param bindingWrapper*/private void registryExchange(BeanDefinitionRegistry registry, BindingWrapper bindingWrapper) {BindingWrapper.ExchangeWrapper exchangeWrapper = bindingWrapper.getExchangeWrapper();AbstractBeanDefinition exchangeBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(this.obtainExchangeType(exchangeWrapper.getType())).addConstructorArgValue(exchangeWrapper.getExchangeName()).addConstructorArgValue(exchangeWrapper.isDurable()).addConstructorArgValue(exchangeWrapper.isAutoDelete()).getBeanDefinition();registry.registerBeanDefinition(exchangeWrapper.getExchangeName(), exchangeBeanDefinition);}/*** 向容器中注册binding的BeanDefinition* @param registry* @param bindingWrapper*/private void registryBinding(BeanDefinitionRegistry registry, BindingWrapper bindingWrapper) {AbstractBeanDefinition bindingBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(org.springframework.amqp.core.Binding.class).addConstructorArgValue(bindingWrapper.getQueueWrapper().getQueueName()).addConstructorArgValue(Binding.DestinationType.QUEUE).addConstructorArgValue(bindingWrapper.getExchangeWrapper().getExchangeName()).addConstructorArgValue(bindingWrapper.getKey()).addConstructorArgValue(Collections.<String, Object>emptyMap()).getBeanDefinition();registry.registerBeanDefinition(bindingWrapper.getBindingName(), bindingBeanDefinition);}/*** 延迟消费 存储消息的制造延迟效果 的队列 上面的param* @return*/private Map<String, Object> obtainMapForDelayQueue(String delayRoutingKey, String exchangeForConsume, long delayTime) {Map<String, Object> paramsForDelay = new HashMap<>();paramsForDelay.put(X_MESSAGE_TTL_DEFAULT, delayTime == -1 ? TTL_DEFAULT_VALUE : delayTime);//默认10分钟paramsForDelay.put(X_DEAD_LETTER_EXCHANGE, exchangeForConsume);//延迟交换器paramsForDelay.put(X_DEAD_LETTER_ROUTING_KEY, delayRoutingKey);//延迟消费路由keyreturn paramsForDelay;}/*** 和Listener绑定,实际消费消息 的队列 上面的param* @return*/private Map<String, Object> obtainMapForConsumeQueue(boolean needRetry, String dlRoutingKey, String exchangeForDl) {if (!needRetry) {return Collections.emptyMap();}Map<String, Object> paramsForDl = new HashMap<>();paramsForDl.put(X_DEAD_LETTER_EXCHANGE, exchangeForDl);//死信交换器paramsForDl.put(X_DEAD_LETTER_ROUTING_KEY, dlRoutingKey);//死信消费路由keyreturn paramsForDl;}/*** 重试场景下 死信队列 上面的param* @return*/private Map<String, Object> obtainMapForDlQueue(String bindingWrapperForRetry, String exchangeForRetry, long delayTime) {Map<String, Object> paramsForOriginal = new HashMap<>();paramsForOriginal.put(X_DEAD_LETTER_EXCHANGE, exchangeForRetry);//重试交换器paramsForOriginal.put(X_DEAD_LETTER_ROUTING_KEY, bindingWrapperForRetry);//重试消费路由keyparamsForOriginal.put(X_MESSAGE_TTL_DEFAULT, delayTime == -1 ? TTL_DEFAULT_VALUE : delayTime);//默认10分钟return paramsForOriginal;}/*** 根据注解中的属性值,返回对应的交换机类型* @param exchangeTypes* @return*/private Class<?> obtainExchangeType(String exchangeTypes) {switch (exchangeTypes) {case DIRECT:return DirectExchange.class;case FANOUT:return FanoutExchange.class;case HEADERS:return HeadersExchange.class;case TOPIC:default:return TopicExchange.class;}}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {//do nothing}@Overridepublic void setEnvironment(Environment environment) {this.environment = environment;}
}
MQ组件配置关系图

相关文章:
关于 自定义的RabbitMQ的RabbitMessageContainer注解-实现原理
概述 RabbitMessageContainer注解 的主要作用就是 替换掉Configuration配置类中的各种Bean配置; 采用注解的方式可以让我们 固化配置,降低代码编写复杂度、减少配置错误情况的发生,提升编码调试的效率、提高业务的可用性。 为什么说“降低…...
uniapp快速入门系列(1)- 概述与基础知识
章节三:抖音小程序页面开发 第1章:概述与基础知识1.1 uniapp简介1.1.1 什么是uniapp?1.1.2 为什么选择uniapp?1.1.3 uniapp与微信小程序的关系 1.2 HBuilderX介绍与安装1.2.1 什么是HBuilderX?1.2.2 HBuilderX的安装1.…...
国密国际SSL双证书解决方案,满足企事业单位国产国密SSL证书要求
近年来,为了摆脱对国外技术和产品的依赖,建设安全的网络环境,以及加强我国对网络信息的安全可控能力,我国推出了国密算法。同时,为保护网络通信信息安全,更高级别的安全加密数字证书—国密SSL证书应运而生。…...
LabVIEW开发虚拟与现实融合的数字电子技术渐进式实验系统
LabVIEW开发虚拟与现实融合的数字电子技术渐进式实验系统 数字电子技术是所有电气专业的重要学科基础,具有很强的理论性和实践性。其实验是提高学生分析、设计和调试数字电路能力,培养学生解决实际问题的工程实践能力,激发学生创新意识&…...
机器学习之单层神经网络的训练:增量规则(Delta Rule)
文章目录 权重的调整单层神经网络使用delta规则的训练过程 神经网络以权值的形式存储信息,根据给定的信息来修改权值的系统方法称为学习规则。由于训练是神经网络系统地存储信息的唯一途径,因此学习规则是神经网络研究中的一个重要组成部分 权重的调整 (…...
C# Task任务详解
文章目录 前言Task返回值无参返回有参返回 async和await返回值await搭配使用Main async改造 Task进阶Task线程取消测试用例超时设置 线程暂停和继续测试用例 多任务等最快多任务全等待 结论 前言 Task是对于Thread的封装,是极其优化的设计,更加方便了我…...
百度网盘的扩容
百度网盘的扩容怎么扩 百度网盘的扩容通常需要购买额外的存储空间。以下是扩容百度网盘存储空间的一般步骤: 登录百度网盘:首先,在您的计算机或移动设备上打开百度网盘,并使用您的百度账号登录。 选择扩容选项:一旦登…...
Android 悬浮窗
本文参考文章地址:https://juejin.cn/post/7009180088310693919 一、申请权限 <uses-permission android:name"android.permission.SYSTEM_ALERT_WINDOW" />二、创建悬浮窗service <serviceandroid:name".FloatingWindowService"an…...
3.物联网射频识别,(高频)RFID应用ISO14443-2协议
一。ISO14443-2协议简介 1.ISO14443协议组成及部分缩略语 (1)14443协议组成(下面的协议简介会详细介绍) 14443-1 物理特性 14443-2 射频功率和信号接口 14443-3 初始化和防冲突 (分为Type A、Type B两种接口&…...
数据分析笔记1
数据分析概述:数据获取--探索分析与可视化--预处理--分析建模--模型评估 数据分析含义:利用统计与概率的分析方法提取有用的信息,最后进行总结与概括 一、数据获取 实用网站:kaggle 阿里云天池 数据仓库:将所有业务数据…...
paramiko 3
import paramiko import concurrent.futuresdef execute_remote_command(hostname, username, password, command):try:# 创建SSH客户端client paramiko.SSHClient()client.set_missing_host_key_policy(paramiko.AutoAddPolicy())# 使用密码认证连接远程主机client.connect(h…...
基于Dlib训练自已的人脸数据集提高人脸识别的准确率
前言 由于图像的质量、光线、角度等因素影响。这时如果使用官方提供的模型做人脸识别,就会导至识别率不是很理想。人脸识别的准确率与图像的清晰度和质量有关。如果图像模糊、光线不足或者有其他干扰因素,Dlib 可能无法正确地识别人脸。为了确保图像质量…...
Git 详细安装教程(详解 Git 安装过程的每一个步骤
Git 详细安装教程(详解 Git 安装过程的每一个步骤) 该文章详细具体,值得收藏学习...
kafka伪集群部署,使用KRAFT模式
1:拉去管理kafka界面UI镜像 docker pull provectuslabs/kafka-ui2:拉去管理kafka镜像 docker pull bitnami/kafka3:docker-compose.yml version: 3.8 services:kafka-1:container_name: kafka1image: bitnami/kafka ports:- "19092:19092"- "19093:19093&quo…...
【双指针遍历】N数之和问题
文章目录 二数之和LC1三数之和LC15四数之和LC18最接近的三数之和LC16 二数之和LC1 题目链接 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 你可以假设每种输入只会对…...
Qt的QObject类
文章目录 QObject类如何在Qt中使用QObject的信号与槽机制?如何在Qt中使用QObject的属性系统?QObject的元对象系统如何实现对象的反射功能? QObject类 Qt的QObject类是Qt框架中的基类,它是所有Qt对象的父类。QObject提供了一些常用…...
【图论C++】链式前向星(图(树)的存储)
/*** file * author jUicE_g2R(qq:3406291309)————彬(bin-必应)* 一个某双流一大学通信与信息专业大二在读 * * brief 一直在竞赛算法学习的路上* * copyright 2023.9* COPYRIGHT 原创技术笔记:转载需获得博主本人…...
16.PWM输入捕获示例程序(输入捕获模式测频率PWMI模式测频率和占空比)
目录 输入捕获相关库函数 输入捕获模式测频率 PWMI模式测频率和占空比 两个代码的接线图都一样,如下 测量信号的输入引脚是PA6,信号从PA6进来,待测的PWM信号也是STM32自己生成的,输出引脚是PA0。 需要配置电路连接图示如下&…...
pip version 更新
最近报了一个错: 解决办法: 在cmd输入“conda install pip” conda install pip 完了之后再输入: python -m pip install --upgrade pip ok....
Oracle - 多区间按权重取值逻辑
啰嗦: 其实很早就遇到过类似问题,也设想过,不过一致没实际业务需求,也就耽搁了;最近有业务提到了,和同事讨论,各有想法,所以先把逻辑整理出来,希望有更好更优的解决方案;…...
边缘计算医疗风险自查APP开发方案
核心目标:在便携设备(智能手表/家用检测仪)部署轻量化疾病预测模型,实现低延迟、隐私安全的实时健康风险评估。 一、技术架构设计 #mermaid-svg-iuNaeeLK2YoFKfao {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
FastAPI 教程:从入门到实践
FastAPI 是一个现代、快速(高性能)的 Web 框架,用于构建 API,支持 Python 3.6。它基于标准 Python 类型提示,易于学习且功能强大。以下是一个完整的 FastAPI 入门教程,涵盖从环境搭建到创建并运行一个简单的…...
PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
dify打造数据可视化图表
一、概述 在日常工作和学习中,我们经常需要和数据打交道。无论是分析报告、项目展示,还是简单的数据洞察,一个清晰直观的图表,往往能胜过千言万语。 一款能让数据可视化变得超级简单的 MCP Server,由蚂蚁集团 AntV 团队…...
USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...
08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险
C#入门系列【类的基本概念】:开启编程世界的奇妙冒险 嘿,各位编程小白探险家!欢迎来到 C# 的奇幻大陆!今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类!别害怕,跟着我,保准让你轻松搞…...
