关于 自定义的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组件配置关系图

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

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

相关文章

QT编译报错stdlib.h:No such file or directory

如图&#xff0c;需要将第19行&#xff0c;INCLUDEPATH /usr/include 注释掉 原因分析&#xff1a; 在Ubuntu的CSTDLIB中&#xff0c;使用的是#include_next下“stdlib.h” &#xff0c;自行增加/usr/include 把include_next的顺序打乱&#xff0c;造成编译错误。但是在cento…

Ubuntu 20.04编译GPMP2过程记录

前言 GPMP2是董靖博士等人在16-17年提出的结合GTSAM因子图框架与Gaussian Processes完成motion planning的一项工作。前身源于Barfoot教授的课题组提出的STEAM(Simultaneous Trajectory Estimation and Mapping)问题及其相关工作。在提出董靖博士提出GPMP2后&#xff0c;borgl…

同步、异步

何为同步、异步&#xff1f; 同步任务&#xff08;synchronous&#xff09; 同步任务指的是&#xff0c;在主线程上排队执行的任务&#xff0c;只有前一个任务执行完毕&#xff0c;才能执行后一个任务&#xff1b;同步任务进栈顺序&#xff1a;先进后出&#xff0c;后进先出&…

网页采集工具-免费的网页采集工具

在当今数字化时代&#xff0c;网页采集已经成为了众多领域的必备工具。无论是市场研究、竞争情报、学术研究还是内容创作&#xff0c;网页采集工具都扮演着不可或缺的角色。对于许多用户来说&#xff0c;寻找一个高效、免费且易于使用的网页采集工具太不容易了。 147SEO工具的强…

Go-Ldap-Admin | openLDAP 同步钉钉、企业微信、飞书组织架构实践和部分小坑

目录 一、Docker-compose快速拉起demo测试环境 二、原生部署流程 安装MySQL&#xff1a;5.7数据库 安装openLDAP 修改域名&#xff0c;新增con.ldif 创建一个组织 安装OpenResty 下载后端 下载前端 部署后端 部署前端 三、管理动态字段 钉钉 企业微信 飞书 四、…

基于微信小程序的刷题考试系统设计与实现(适用于各类考试类、答题类程序)

文章目录 前言系统主要功能&#xff1a;具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计…

JavaScript Web APIs第二天笔记

Web APIs - 第2天 学会通过为DOM注册事件来实现可交互的网页特效。 能够判断函数运行的环境并确字 this 所指代的对象理解事件的作用&#xff0c;知道应用事件的 3 个步骤 学习会为 DOM 注册事件&#xff0c;实现简单可交互的网页特交。 事件 事件是编程语言中的术语&#xff…

(一)gitblit安装教程

(一)gitblit安装教程 (二) gitblit用户使用教程 (三) gitblit管理员手册 目录 前言安装1.下载Java Runtime Requirement 2.设置环境变量3.gitblit内容3.1 gitblit文件夹内容3.2 defaults.properties 主要配置选项 4 配置4.1 准备文件4.2 修改gitblit.properties4.3 修改authori…

基于自适应启动策略的混合交叉动态约束多目标优化算法(MC-DCMOEA)求解CEC2015/CEC2018/CEC2023(MATLAB代码)

一、动态多目标优化问题 1.1问题定义 1.2 动态支配关系定义 二、 基于自适应启动策略的混合交叉动态多目标优化算法 基于自适应启动策略的混合交叉动态多目标优化算法&#xff08;Mixture Crossover Dynamic Constrained Multi-objective Evolutionary Algorithm Based on Se…

试图一文彻底讲清 “精准测试”

在软件测试中&#xff0c;我们常常碰到两个基本问题&#xff08;困难&#xff09;&#xff1a; 很难保障无漏测&#xff1a;我们做了大量测试&#xff0c;但不清楚测得怎样&#xff0c;对软件上线后会不会出问题&#xff0c;没有信心&#xff1b; 选择待执行的测试用例&#…

跨类型文本文件,反序列化与类型转换的思考

文章目录 应用场景序列化 - 对象替换原内容&#xff0c;方便使用编写程序取得结果数组 序列化 - JSON 应用场景 在编写热更新的时候&#xff0c;我发现了一个古早的 ini 文件&#xff0c;记录了许多有用的数据 由于使用的语言年份较新&#xff0c;没有办法较好地对 ini 文件的…

map和set的具体用法 【C++】

文章目录 关联式容器键值对setset的定义方式set的使用 multisetmapmap的定义方式insertfinderase[]运算符重载map的迭代器遍历 multimap 关联式容器 关联式容器里面存储的是<key, value>结构的键值对&#xff0c;在数据检索时比序列式容器效率更高。比如&#xff1a;set…

ARP欺骗攻击实操

目录 目录 前言 系列文章列表 全文导图 1&#xff0c;ARP概述 1.1,ARP是什么&#xff1f; 1.2,ARP协议的基本功能 1.3,ARP缓存表 1.4,ARP常用命令 2&#xff0c;ARP欺骗 2.1,ARP欺骗的概述? 2.2,ARP欺骗的攻击手法 3&#xff0c;ARP攻击 3.1,攻击前的准备 3.2,…

【Spring Boot】实战:实现数据缓存框架

🌿欢迎来到@衍生星球的CSDN博文🌿 🍁本文主要学习【Spring Boot】实现数据缓存框架 🍁 🌱我是衍生星球,一个从事集成开发的打工人🌱 ⭐️喜欢的朋友可以关注一下🫰🫰🫰,下次更新不迷路⭐️💠作为一名热衷于分享知识的程序员,我乐于在CSDN上与广大开发者…

Python3 如何实现 websocket 服务?

Python 实现 websocket 服务很简单&#xff0c;有很多的三方包可以用&#xff0c;我从网上大概找到三种常用的包&#xff1a;websocket、websockets、Flask-Sockets。 但这些包很多都“年久失修”&#xff0c; 比如 websocket 在 2010 年就不维护了。 而 Flask-Sockets 也在 2…

SQL血缘解析原理

根据sql解析获取到表到表, 字段到字段间的关系,即血缘关系。实际上这是从sql文本获取到数据流的过程。 大致步骤如下&#xff1a; 1.sql文本进行词法分析 2.sql语法分析获取到AST抽象语法树 3.访问AST抽象语法树根据语法结构推测出数据的流向,例如create as select from 这种结…

[vue-admin-template实战笔记]

1.克隆项目 git clone gitgitee.com:panjiachen/vue-admin-template.git 2.安装依赖 npm install 3.运行项目就会自动打开网页&#xff0c;并且热部署插件 npm run dev 4.查看代码 //将vue-admin-template拖入到idea中即可查看代码 1)并且发现&#xff0c;常用的东西已经集…

Machine Learning(study notes)

There is no studying without going crazy Studying alwats drives us crazy 文章目录 DefineMachine LearningSupervised Learning&#xff08;监督学习&#xff09;Regression problemClassidication Unspervised LearningClustering StudyModel representation&#xff08…

unity 鼠标标记 左键长按生成标记右键长按清除标记,对象转化为子物体

linerender的标记参考 unity linerenderer在Game窗口中任意画线_游戏内编辑linerender-CSDN博客 让生成的标记转化为ARMarks游戏对象的子物体 LineMark.cs using System.Collections; using System.Collections.Generic; using UnityEngine;public class LineMark : MonoBeh…

excel筛选后求和

需要对excel先筛选&#xff0c;后对“完成数量”进行求和。初始表格如下&#xff1a; 一、选中表内任意单元格&#xff0c;按ctrlshiftL&#xff0c;开启筛选 二、根据“部门”筛选&#xff0c;比如选择“一班” 筛选完毕后&#xff0c;选中上图单元格&#xff0c;然后按alt后&…