当前位置: 首页 > news >正文

Spring 源码解析 - Bean创建过程 以及 解决循环依赖

一、Spring Bean创建过程以及循环依赖

上篇文章对 Spring Bean资源的加载注册过程进行了源码梳理和解析,我们可以得到结论,资源文件中的 bean 定义信息,被组装成了 BeanDefinition 存放进了 beanDefinitionMap 容器中,那 Bean 是怎样创建和依赖注入的还没有进行分析,而且这里还有个经典的循环依赖问题,本篇文章将带领大家一起继续上篇文章进行Spring源码的分析,梳理下Bean创建过程以及循环依赖问题的解决。

下面是上篇文章的地址:

Spring 源码解析 - Bean资源加载注册过程

在这里插入图片描述

在上篇文章,我们主要分析的 AbstractApplicationContextrefresh() 方法中的 obtainFreshBeanFactory() 方法的作用,通过该方法我们了解到如何创建的 BeanFactory 工厂,以及如何生成 BeanDefinition 并注册到容器,本篇文章我们还是从 refresh() 方法中找突破点,从 finishBeanFactoryInitialization(beanFactory) 入手分析,该方法主要做了对所有的单例类型 bean 进行初始化以及依赖注入:

在这里插入图片描述

二、finishBeanFactoryInitialization(beanFactory)

在这里插入图片描述
点击到该方法中,直接看到方法最后的 beanFactory.preInstantiateSingletons() 方法中:

在这里插入图片描述

preInstantiateSingletons() 方法中,可以明显看到拿到了 beanDefinitionNames 列表,而这个列表则是存储所有注册的 beanDefinition 定义信息。

下面接着判断是否为单例模式,并且不是懒加载,这种情况下在项目启动时才需要进行实例化。

在这里插入图片描述

下面会判断当前的 beanName 是否为 FactoryBean ,如果不是的话则使用 getBean(beanName) ,从这命名上我们也可以猜出 getBean(beanName) 肯定是做了对 bean 的创建,下面主要分析该方法,其实该方法调用的是AbstractBeanFactory 下的 getBean(beanName)

在这里插入图片描述

这里又调用了当前类的 doGetBean 方法,到这之后,我们先不着急继续向下分析。

当我们在使用 Spring 时,需要使用 bean 时,则一般都是通过 ApplicationContextgetBean(String name) 方法获取一个指定的对象,使用方式如下图所示:
在这里插入图片描述
那这个 getBean(String name) 方法和上面我们分析得到的 getBean(beanName) 是否为一个呢?下面我们可以先看下 ApplicationContext 类的 getBean(String name) 方法最终使用的哪个方法来获取 bean

三、ApplicationContext 下的 getBean(String name) 方法

ApplicationContext 中并没有重写 getBean(String name) 方法,实际的 getBean(String name) 其实是调用的 AbstractApplicationContext下的 getBean(String name)

在这里插入图片描述
在该方法中又使用了当前的 BeanFactory 中的 getBean(String name, Class<T> requiredType) 方法,那此时的 BeanFactory 是具体哪个子类呢?

在上篇文章中我们得出结论中,其中 BeanFactory 默认是使用 DefaultListableBeanFactory,而 DefaultListableBeanFactory 并没有对 getBean(String name) 方法进行重写,那继续去父类 AbstractAutowireCapableBeanFactory 中找,也没有对 getBean(String name) 方法重写,继续向父类 AbstractBeanFactory 找,发现有 getBean(String name) 的重写。

在这里插入图片描述

看到这里是不是很有印象,和前面初始化单例分析时调用了相同的方法进行初始化 bean , 那此时我们就可以得出结论不管是单例还是原型或者其他,最终都是使用的 AbstractBeanFactory 中的 getBean(String name) 方法初始并获取 bean

下面我们重点分析下,其中的 doGetBean 方法。

四、doGetBean 以及 循环依赖

在这里插入图片描述
doGetBean 方法中,首先对传入的 name 进行转换,因为 name 可能是别名,需要转为 beanName

接着使用 getSingleton(String beanName) 方法获取该 beanName 的实例,如果实例不为空的话则使用该 bean

这里的 getSingleton(String beanName) 其实是在 DefaultSingletonBeanRegistry 类下,主要是从单例缓存中获取已经创建的对象,这里就使用到了经典的三级缓存。

在这里插入图片描述

在该方法中又调用了当前类的 getSingleton(String beanName, boolean allowEarlyReference) 方法,继续进到方法下:

在这里插入图片描述

从该方法中可以明显看出,首先去 singletonObjects 容器中去获取对象,如果不存在的话则再去 earlySingletonObjects 容器中获取对象,如果还不存在的话,则从 singletonFactories 容器中获取一个 ObjectFactory ,如果存在则可以通过 ObjectFactory 创建对象,最后将对象存放至 earlySingletonObjects 容器中,然后从singletonFactories 容器中移除。

这里如果不了解循环依赖的话,是不是看着一脸懵。

什么是循环依赖呢,如下图所示的这种都属于循环依赖:

在这里插入图片描述
由于 Spring 在实例化对象时,会先去尝试创建其依赖的对象,这样就会出现 A 在创建中,发现依赖于 B,此时再去创建 B,而 B 又依赖于 A 再去尝试创建 A ,这种情况下不就陷入了死循环中,因此为了解决这种场景增加了多级缓存的概念。

多级缓存是哪几级呢,其实就是上面的 singletonObjects、earlySingletonObjects、singletonFactories 三个容器,分别对应着缓存的一二三级,这三个容器在 DefaultSingletonBeanRegistry 类中:

在这里插入图片描述

三级缓存具体是怎么使用的呢:

一级缓存:singletonObjects,存储所有的单例bean
二级缓存:earlySingletonObjects,存储提前暴露的bean,里面的bean都是没创建完的
三级缓存:singletonFactories,存储单例 beanObjectFactory(也就是创建该bean的工厂)

创建对象时首先会将创建 beanObjectFactory 暴露至三级缓存中,后面如果有依赖于该 bean 的则会先从一二三级缓存中挨个尝试取数据,直接拿到,如果是在三级缓存中存在,则通过 ObjectFactory获取一个早期的 bean 对象,并且放入二级缓存中,三级缓存中的移除,后面使用该早期的bean对象进行依赖注入。再后面当该早期 bean 的依赖也被注入完成后,此时就是一个完整的对象了,需要从二级缓存中移除,放入一级缓存中,供上层使用。

了解这些之后,就不难理解 getSingleton(String beanName, boolean allowEarlyReference) 方法中的逻辑了,下面在回到 AbstractBeanFactory 类的 doGetBean 方法下:

在这里插入图片描述

如果从 getSingleton(String beanName) 中获取的实例为空,则缓存中不存在,那就要看如果进行实例化的了。

这里首先会判断是否原型模式下形成了循环依赖,如果是则抛出异常,因此这里可以得出 Spring 原型模式下不允许出现循环依赖。

这里可以看下 isPrototypeCurrentlyInCreation 方法。

在这里插入图片描述
在这里插入图片描述

可以看到这里的prototypesCurrentlyInCreation其实就是一个 ThreadLocal ,判断的依据就是当前 ThreadLoad 等于或者包含该 beanName ,即认为是出现了循环依赖,那该 ThreadLocal 什么时候写数据的呢,再回到 doGetBean 方法中,继续向下分析。

在这里插入图片描述

接着会获取到上级的 BeanFactory ,并且如果当前的 beanName 不在 beanDefinitionMap 容器中,则尝试用上级的 BeanFactory 进行获取 bean ,一般对于我们声明好的 bean 肯定都会存在于 beanDefinitionMap 容器中。

在这里插入图片描述

再接着会根据 beanDefinitionMap 容器中的 BeanDefinition 创建一个 RootBeanDefinition ,接着会遍历全部的依赖,首先判断是否已经注册依赖了,主要判断 dependentBeanMap 容器中是否存在,如果不存在的话,则进行注册将该依赖加入到dependentBeanMap 中,最后递归的方式调用 getBean(String name) 创建依赖的对象。

在这里插入图片描述

在接着就要进行当前 bean 的创建工作了,首先如果是单例模式下,则使用 getSingleton(String beanName, ObjectFactory<?> singletonFactory) 方法获取实例对象,并传入一个 ObjectFactory 函数,其中 getSingleton 方法其实在 DefaultSingletonBeanRegistry下,进入到该方法中:

在这里插入图片描述

在这里插入图片描述

在该方法中首先对一级缓存进行上锁,然后再次尝试从一级缓存中获取对象,如果还是不存在的话,并且单例对象正在销毁的话则抛出异常,否则的话则执行创建 bean 的前方法,主要写入 singletonsCurrentlyInCreation 容器中,表示当前 bean 正在创建中。这个容器名称可以记一下会在后面进行判断是否是创建中的 bean

在这里插入图片描述

下面接着使用 newSingleton 作为创建对象的标记,可以看到当调用 singletonFactory.getObject() 成功后,则置为成功的标志,而 singletonFactory 则就是前面传递进来的 ObjectFactory,这里的 getObject() ,其实就是执行的 AbstractAutowireCapableBeanFactory 下的 createBean 方法,该方法可以放在后面分析,因为原型模式也是使用的 createBean 方法创建实例对象,后面可以一起分析,现在我们知道它是创建实例对象的就可以。

在这里插入图片描述

在接着该方法的最后,如果 newSingleton标记创建成功,则此时对象属于一个完整的对象,其中依赖注入已经在 createBean 方法中实现过了,这个时候需要写入到一级缓存中曝光出来。

在这里插入图片描述
在写入一级缓存时,首先对一级缓存加锁,然后写入一级缓存,并从二三级缓存移除,同时记录到已注册的单例列表中。

到此 getSingleton(String beanName, ObjectFactory<?> singletonFactory) 方法就走完了,在回到AbstractBeanFactorydoGetBean 继续向下看。

在这里插入图片描述
上面如果不是单例模式,再继续判断否为原型模式,如果是原型的,则先调用 beforePrototypeCreation(String beanName)beanName 加入到 prototypesCurrentlyInCreation ThreadLocal 中,进入到该类中逻辑如下:

在这里插入图片描述

这里 prototypesCurrentlyInCreation ThreadLocal 正对应上前面原型模式下判断循环依赖,下面在回到上一步方法中,可以看到和单例模式一样使用了 AbstractAutowireCapableBeanFactory 下的 createBean 方法创建实例,同样该方法还是放在后面分析,继续向下看 afterPrototypeCreation(String beanName) 方法,此时当前 bean 已经实例化好了,依赖注入同样在createBean 方法中已经完成了,下面就可以将 ThreadLoad 中记录的 beanName 移除了,逻辑如下:

在这里插入图片描述

在回到上一步方法中,继续向下看:

在这里插入图片描述
接着在 else 中则表示,既不是单例模式也不是原型模式下,这里就不做过多的介绍了,但明显的可以看到也是使用 createBean 方法创建实例对象的。

在这里插入图片描述

看到最后的话,三种情况下创建的 bean 不为空的话,则直接返回给上层,也就是给到业务层可以正常使用该 bean 了。

五、createBean

从上面的分析中发现,实际创建对象都使用的 AbstractAutowireCapableBeanFactory 下的 createBean 方法,现在我们对该方法分析下是如何操作的,进入到该方法中:

在这里插入图片描述

这里首先拿到前面创建的 RootBeanDefinition,下面根据 RootBeanDefinition 获取到当前 beanClass,主要判断需要创建的bean 是否可以被实例化,是否可以通过当前的类加载器加载。 如果 resolvedClass 不为空,并且 mbdbeanClass 不是 resolvedClass 话,则创建一个新的 RootBeanDefinition

在这里插入图片描述

接着会尝试创建代理对象,如果是 AOP 的话则尝试使用 BeanPostProcessors 来替代真正的实例,对于 AOP 的分析这里不做过多的介绍了,这里主要关注下最后的 doCreateBean 方法,进入到该方法中:

在这里插入图片描述

doCreateBean 方法中,如果是单例模式的话,会尝试从 factoryBeanInstanceCache 缓存中获取一个 BeanWrapper 压缩对象,如果不存在的话则通过 createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) 方法创建一个新的 BeanWrapper 对象。

这里的BeanWrapper 对象是对bean的包装,可以使用统一的方式来访问bean的属性,从下面的操作可以看出,通过BeanWrapper 对象获取到了当前 bean 的实例对象,因此在 createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) 方法中肯定对当前 bean 进行了实例化操作,下面进入到该方法中:

在这里插入图片描述

createBeanInstance 方法中,首先通过 BeanDefinition 获得 beanClass ,下面如果 RootBeanDefinition 的工厂方法不为空则使用工厂方法进行初始化策略。

在这里插入图片描述

如果 RootBeanDefinition 的工厂方法为空,接着向下看,其中 resolved 表示是否已经确定好构造方法,autowireNecessary 表示构造方式是否需要自动注入,如果传递过来的参数为空的话,那这个时候就对RootBeanDefinitionconstructorArgumentLock 进行上锁,接着会判断 RootBeanDefinitonresolverConstructorOrFactoryMethod 是否为空,这里其实是一个缓存,后面在实例对象时会对其进行赋值,这里如果存在缓存,则直接使用。

在这里插入图片描述
下面如果构造方法已经确定的话,那就是 mbd.resolvedConstructorOrFactoryMethod 缓存存在,则根据 mbd 中的构造类型选择用有参的构造函数创建 bean,还是使用无参的构造函数创建 bean

这里我们假设缓存为空的话,继续向下执行。

在这里插入图片描述

如果没有缓存的话,则就要找beanName 对应的 beanClass 的所有的有参构造器,如果找到了则尝试进行创建 bean ,否则的话直接使用无参的构造函数进行初始化,这里由于篇幅我们直接分析下 instantiateBean 无参构造函数的实例化。

进入 instantiateBean 方法中。

在这里插入图片描述

在该方法中没如果是否开启了安全管理,如果是则使用 AccessController.doPrivileged 生成 bean 实例,可以看到最终都是由 getInstantiationStrategy().instantiate 方法生成实例,该方法其实在 SimpleInstantiationStrategy 类中,下面进入到该方法中:

在这里插入图片描述

在该方法中,首先如果有覆盖方法的话,同样对 RootBeanDefinitionconstructorArgumentLock 进行上锁,接着也还是获取 RootBeanDefinitonresolverConstructorOrFactoryMethod 是否为空,就是判断是否存在缓存,如果不存在的话则获取到 beanClass ,如果 Class 是接口类型的话则抛出异常。

在这里插入图片描述

这一步的处理比较明了,如果是安全模式下,则使用 AccessController.doPrivileged ,同时这里使用 clazz.getDeclaredConstructor 反射获取 class 的构造方法,并且将反射得到的结果存入到resolvedConstructorOrFactoryMethod 中进行缓存,对应了前面取缓存的动作,最后直接通过 BeanUtils 工具类反射实例化出 bean 对象。

下面如果没有覆盖方法的话,则使用 cglib 进行实例化对象:

在这里插入图片描述

看到这里之后,我们应该了解到了 bean 在哪里被创建的了,下面在回到原来的 AbstractAutowireCapableBeanFactory 下的 doCreateBean 方法中,其中 createBeanInstance 已经创建出了 bean 实例,下面继续向下看:

在这里插入图片描述
这里判断是否为单例模式,并且允许循环依赖,并且该 beanName 正在创建中的话,判断是否正在创建中就是去 singletonsCurrentlyInCreation 容器中判断有无,这个容器在前面有提到。

如果条件都符合的话,则首先曝光至三级缓存中,可以看下 addSingletonFactory 方法的过程:

在这里插入图片描述
这里首先对一级缓存进行上锁,并且一级缓存中不存在,则写入到三级缓存中,并从二级缓存中移除,最后加入到注册单例列表中。

下面回到上一步的方法中。

在这里插入图片描述
接着会有一个核心的方法 populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) 方法,主要进行了依赖的注入,依赖注入完后接着调用 beaninit-method 方法,进行自定义的初始化操作,这里我们主要看下 populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) 方法。

在这里插入图片描述

populateBean 方法中,先校验是否需要进行依赖注入,接着向下看,可以看到两个非常显眼的标识:

在这里插入图片描述

这里进行了两种类型的解析 byNamebyType 就是配置的 autowire 属性 ,选择依赖注入的方式是根据名称还是根据类型,这里我们主要看下 autowireByName 的逻辑:

在这里插入图片描述

这里首先获取到所有依赖的 beanName ,然后如果存在容器中的话,则递归的方式获取到该 bean ,下面记录到上一层的 newPvs 中。

接着再回到上一层的方法中,接着向下看,此时依赖的属性已经都被存放在了 pvs 中,可以看到最后是使用 applyPropertyValues 方法进行属性的赋值,下面来看下该方法的逻辑:

在这里插入图片描述
在这里插入图片描述
applyPropertyValues 方法中同样判断了是否开启安全管理,继续向下看:

在这里插入图片描述

这里首先判断了下属性是否被转化,如果转化了就直接设置属性值,否则的话就记录下,在后面的逻辑中对其进行转化后再进行设置属性值。

这个转换其实就是依赖的类型有可能多种多样,比如可能是引用类型、集合类型、字符串、Properties属性等等,对于不同的依赖需要由不同的操作方式,因此在下面的逻辑中生成了一个 BeanDefinitionValueResolver 这个是 bean 定义属性值解析器,主要就是将 bean 定义中的属性值解析为 bean 实例对象的实际值:

在这里插入图片描述

这里上面没有被转化的属性,这里会进行转化,下面主要看是如何进行转化的,进入到 resolveValueIfNecessary 方法中:

在这里插入图片描述
在这里插入图片描述

可以明显看出针对不同的类型进行了不同的解析方式,这里以引用类型为例看下resolveReference(Object argName, RuntimeBeanReference ref) 方法是如何处理的:

在这里插入图片描述

这里的逻辑也非常直观,首先获取到 beanName ,如果不是引用父类容器中的 bean 的话,则使用 getBean 递归获取依赖的对象,最后对其注册,并返回给上层。

下面再回到 applyPropertyValues 方法中就好理解了,最后将转化后的值通过 PropertyValue 中的 setConvertedValue 方法注入到 bean 的属性字段中。

在这里插入图片描述

到这里就大概看完 bean 的依赖注入,下面我们再回到 AbstractAutowireCapableBeanFactory 中的 doCreateBean 方法中,继续向下走的话

在这里插入图片描述

这里又从缓存中获取一遍 bean ,但这里的 allowEarlyReference 属性为 false ,也就是最多只会走一二级缓存,如果取不到的话,直接将前面的实例对象返回,否则如果是被代理的话,再次检查一遍依赖是否都加载完。

最后将处理的 bean 返回出去。

看到这里应该会对Bean的创建过程以及循环依赖有了一定的理解了吧。

相关文章:

Spring 源码解析 - Bean创建过程 以及 解决循环依赖

一、Spring Bean创建过程以及循环依赖 上篇文章对 Spring Bean资源的加载注册过程进行了源码梳理和解析&#xff0c;我们可以得到结论&#xff0c;资源文件中的 bean 定义信息&#xff0c;被组装成了 BeanDefinition 存放进了 beanDefinitionMap 容器中&#xff0c;那 Bean 是…...

移除元素(双指针)

给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用 O(1) 额外空间并原地修改输入数组。 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的…...

76.qt qml-QianWindow开源炫酷界面框架(支持白色暗黑渐变自定义控件均以适配)

界面介绍界面支持: 透明 白色 黑色 渐变 单色 静态图 动态图侧边栏支持:抽屉、带折叠、多模式场景控件已集成: 暗黑风格 高亮风格、并附带个人自定义控件及开源demo白色场景如下所示:单色暗黑风格如下所示:用户自定义皮肤如下所示:皮肤预览如下所示:b站入口:https://www.bilibi…...

Python生日蛋糕

目录 前言 底盘 蛋糕 蜡烛 祝福 前言 Hello&#xff0c;小伙伴们晚上好吖&#xff01;前两天博主满20岁啦&#xff08;要开始奔三辽呜呜呜&#xff09;&#xff0c;这几天收到了不少小伙伴们的祝福&#xff0c;浪漫的小博主想送给大家一份不一样的生日蛋糕&#xff0c…...

QT 如何提高 Qt Creator 的编译速度

如何提高编译速度&#xff0c;貌似是一个老生常谈的话题。对于Qter而言&#xff0c;如何提高QT Creator 的编辑速度是一直都是大家所期盼的。本文也是查阅了各路大神的方法后整理出来的&#xff0c;希望对各位有所帮助。 1、在*.pro文件添加预编译机制 QT官方给出的示例&…...

STM32之震动传感器、继电器介绍及实战

目录 一、震动传感器介绍及实战 二、编程代码实现 1、gpio.c---------初始化GPIO口引脚函数 2、调用中断服务函数 3、中断服务函数 4、中断服务回调函数 5、把上述的中断服务回调函数&#xff0c;放入main主函数里 6、结果演示 三、继电器介绍及实战 一、震动传感器介…...

RK3588平台开发系列讲解(显示篇)RK3588 平台 的DP介绍

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、功能特性二、 DP 输⼊三、DP 输出四、 代码路径沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇将介绍 RK3588 平台 DP 的使⽤与调试⽅法。 一、功能特性 RK3588 的 DP ⽀持 1.4a 版本的 DP 协议,最…...

【Java】i++和++i的实现原理

文章目录 i++案例反编译分析扩展 x = x++我们接下来从字节码层面分析: 不了解字节码的可以参考这篇:【精通JVM】字节码指令全解 i++案例 package org.example;public class Main {public static void main...

第十四届蓝桥杯三月真题刷题训练——第 18 天

目录 第 1 题&#xff1a;排列字母 问题描述 运行限制 代码&#xff1a; 第 2 题&#xff1a;GCD_数论 问题描述 输入格式 输出格式 样例输入 样例输出 评测用例规模与约定 运行限制 第 3 题&#xff1a;选数异或 第 4 题&#xff1a;背包与魔法 第 1 题&#x…...

软件测试拿了几个20K offer,分享一波面经

1、你的测试职业发展是什么? 测试经验越多&#xff0c;测试能力越高。所以我的职业发展是需要时间积累的&#xff0c;一步步向着高级测试工程师奔去。而且我也有初步的职业规划&#xff0c;前3年积累测试经验&#xff0c;按如何做好测试工程师的要点去要求自己&#xff0c;不断…...

spring2

1.Spring配置数据源1.1 数据源&#xff08;连接池&#xff09;的作用 数据源(连接池)是提高程序性能如出现的事先实例化数据源&#xff0c;初始化部分连接资源使用连接资源时从数据源中获取使用完毕后将连接资源归还给数据源常见的数据源(连接池)&#xff1a;DBCP、C3P0、BoneC…...

【Linux】网络编程套接字(中)

&#x1f387;Linux&#xff1a; 博客主页&#xff1a;一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限&#xff0c;出现错误希望大家不吝赐教分享给大家一句我很喜欢的话&#xff1a; 看似不起波澜的日复一日&#xff0c;一定会在某一天让你看见坚持…...

手撕数据结构—队列

队列队列的话只允许在一端插入&#xff0c;在另外一端删除。插入数据的那一段叫做队尾&#xff0c;出数据的那一段叫做队头&#xff08;从尾巴插入&#xff09;。因此的话队列是先进先出的。入的顺序与出的顺序的话是一样的。这个与栈是不一样的&#xff0c;因为栈的话就是说如…...

gdb调试工具和makemakefile工具

gdb调试工具和make/makefile工具 文章目录gdb调试工具和make/makefile工具一、gdb调试工具1.debug/release2.使用二、make/makefile1.什么是make/makefile2.编写一、gdb调试工具 1.debug/release 程序有两种默认的发布方式debug和release。release是无法进行调试的。Linux中g…...

【进阶数据结构】平衡搜索二叉树 —— AVL树

&#x1f308;感谢阅读East-sunrise学习分享——[进阶数据结构]AVL树 博主水平有限&#xff0c;如有差错&#xff0c;欢迎斧正&#x1f64f;感谢有你 码字不易&#xff0c;若有收获&#xff0c;期待你的点赞关注&#x1f499;我们一起进步&#x1f680; &#x1f308;我们上一篇…...

ROS使用(5)action学习

action消息的构建 首先进行功能包的创建 mkdir -p ros2_ws/src cd ros2_ws/src ros2 pkg create action_tutorials_interfaces action消息的类型 # Request --- # Result --- # Feedback 动作定义由三个消息定义组成&#xff0c;以---分隔。 从动作客户机向动作服务器发送…...

2023前端面试题集(含答案)之HTML+CSS篇(一)

在又到了金三银四的招聘季&#xff0c;不管你是刚入行的小白&#xff0c;亦或是混迹职场的老鸟&#xff0c;还在为面试前端工程师时不知道面试官要问什么怎么回答而苦恼吗&#xff1f;为了帮助你获得面试官的青睐&#xff0c;顺利通过面试&#xff0c;跳槽进入大厂&#xff0c;…...

设计模式2 - 观察者模式

定义&#xff1a; 观察者模式又叫发布订阅模式&#xff0c;它定义了对象之间的一对多依赖&#xff0c;这样一来&#xff0c;当一个对象改变状态时&#xff0c;它的所有依赖者都会收到通知并自动更新。 组成&#xff1a; Subject&#xff08;通知者/被观察者&#xff09;&#…...

ini配置文件

ini配置文件 ini文件是initialization file的缩写&#xff0c;即初始化文件&#xff0c;是widows系统配置文件所采用的存储格式。 文件扩展名: .ini ini配置文件的后缀名也不一定必须是.ini, 也可以是.cfg, .conf或者是.txt ini文件格式 ini配置文件由参数, 节, 注解组成 参…...

蓝桥杯备赛经验 pythonA组(非科班选手)

个人2022 CA组江苏省一等奖&#xff0c;决赛成绩不理想&#xff0c;没有拿到一二等奖&#xff0c;但是因为自己是非科班的学生&#xff0c;所以能拿到这样的成绩自己其实也应该知足了 题外话&#xff1a; 很多ACMer嘲笑蓝桥杯非常水&#xff0c;但是据我观察CA组决赛一等奖获奖…...

C++实现通讯录管理系统

通讯录是一个可以记录亲人、好友信息的工具&#xff0c;本博客借助黑马程序员的项目进行修改&#xff0c;利用C实现一个通讯录管理系统&#xff0c;旨在复习C的语法。 一、系统需求 系统需要实现的功能如下&#xff1a; 添加联系人∶向通讯录中添加新人&#xff0c;信息包括…...

开关电源Y电容放置的位置

Y电容&#xff0c;是我们工程师做开关电源设计时都要接触到的一个非常关键的元器件&#xff0c;它对EMI的贡献是相当的大的&#xff0c;但是它是一个较难把控的元器件&#xff0c;原理上并没有那么直观易懂&#xff0c;在EMI传播路径中需要联系到很多的寄生参数才能够去分析。 …...

二叉树的最小深度——递归法、迭代法

1题目给定一个二叉树&#xff0c;找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。说明&#xff1a;叶子节点是指没有子节点的节点。示例 1&#xff1a;输入&#xff1a;root [3,9,20,null,null,15,7]输出&#xff1a;2示例 2&#xff1a;输入&…...

Vue中常使用的三种刷新页面的方式

一、通过js原始方法刷新 缺点&#xff1a; 出现闪白 目录 一、通过js原始方法刷新 二、通过Vue自带的路由进行跳转 三、通过在APP页面进行demo进行刷新(推荐) 1.vue2写法 2. vue3.2写法 <template><div><div class"header"><button clic…...

【Shell】脚本

Shell脚本脚本格式第一个Shell脚本&#xff1a;hello.sh脚本常用执行方式1. bash或sh脚本的相对路径或绝对路径2. 输入脚本的绝对路径或相对路径3. 在脚本的路径前加上.或者source脚本格式 脚本以#!/bin/bash开头&#xff08;指定解析器&#xff09; #! 是一个约定的标记&…...

Mybatis的多表操作

1.Mybatis多表查询 1.1一对一查询 1.一对一查询的模型 用户表和订单表的关系为&#xff0c;一个用户有多个订单&#xff0c;一个订单只从属于一个用户 一对一查询的需求&#xff1a;查询一个订单&#xff0c;与此同时查询出该订单所属的用户2.创建Order和User实体public class…...

【JVM】字节码指令全解

文章目录 入门案例原始 java 代码编译后的字节码文件常量池载入运行时常量池方法字节码载入方法区main 线程开始运行,分配栈帧内存执行引擎开始执行字节码bipush 10istore_1ldc #3istore_2iload_1iload_2iaddistore_3getstatic #4iload_3invokevirtual #5return条件判断指令循…...

【精品】华为认证数通HCIA+HCIP题库分享(含答案解析)

嗨~大家好久不见&#xff0c;我是薄荷学姐&#xff0c;随着华为业务也全球领域的迅猛发展&#xff0c;越来越多人开始重视华为认证的重要性。今天给大家分享一下去年8月份的题库&#xff0c;基本都是一样&#xff0c;希望可以帮助到大家哈想要通过华为认证&#xff0c;除了进行…...

Qt cmake 资源文件的加载

Qt cmake 资源文件的加载概述qt_add_resourcesqt5_add_resourcesqt6_add_resources是否需要加载qrc文件需要加载qrc的情况不需要加载qrc的情况C 代码加载示例加载PNG加载CSS文件加载qrc文件Qt6相对于Qt5的一些变化Qt6和Qt5在加载资源文件方面的区别主要集中在两个方面&#xff…...

【链表OJ题(九)】环形链表延伸问题以及相关OJ题

环形链表OJ题 1. 环形链表 链接&#xff1a;141. 环形链表 描述&#xff1a; 给你一个链表的头节点 head &#xff0c;判断链表中是否有环。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&…...