Springboot启动流程分析(四):完成启动流程

news2025/1/20 13:33:42

目录

一 添加BeanPostProcessors到IOC容器

二 国际化支持

三 初始化监听器的多路播放器

四 刷新容器

五 注册监听器到IOC容器的多播器

六 完成bean的大规模实例化

 6.1 大规模实例化bean

6.1.1 连续三层do...while循环作用

6.1.2 FactoryBean是什么?为什么要执行2次getBean方法?

七 完成IOC刷新

八 完成IOC刷新

九 结束Springboot启动流程

十 总结


在Springboot的启动过程中,当执行到

AbstractApplicationContext

的refresh方法时,当执行完方法:

this.invokeBeanFactoryPostProcessors(beanFactory);

一 添加BeanPostProcessors到IOC容器

在执行完所有的BeanFactoryPostProcessors指定方法之后。下一步,会把BeanPostProcessors,也就是beanFacotry中已经注册过bean定义信息的,所有bean的后置处理器,都加入到beanFacotry的集合属性——beanPostProcessors中。

其具体执行方法为:

this.registerBeanPostProcessors(beanFactory);

接着再调用 :

ostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);

开始执行真正的bean后置处理器加载步骤。

其核心步骤,就是通过beanFactory的getBeanNamesForType方法,去beanFacotry的属性——beanDefinitionNames集合中,筛选出实现了BeanPostProcessors接口的所有beanNames,返回一个String数组:

String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

 至此,相当于大概获取了beanFacroty中的全部bean的后置处理器的beanName信息了。

为什么说是大概呢?因为接下来,还需要手动加入一个bean的后置处理器——BeanPostProcessorChecker:

int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
beanFactory.addBeanPostProcessor(new PostProcessorRegistrationDelegate.BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

顾名思义,应该是对bean的后置处理器进行一些统计和检查。

接下来,就是把获取到的postProcessorNames根据属性进行分类,并根据分类的顺序,依次添加到beanFactory的集合属性——beanPostProcessors中。

其分类和添加到beanPostProcessors集合的顺序依次为:

priorityOrderedPostProcessors集合、 
orderedPostProcessorNames集合、 
nonOrderedPostProcessorNames集合、 
internalPostProcessors集合 

 其主要是通过接口PriorityOrdered、Ordered、MergedBeanDefinitionPostProcessor来判断应该把后置处理器放入哪个集合中。

在最后,还需要添加一个指定后置处理器:

beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));

其实这个执行过程和上一篇文章,beanFacotry后置处理器的过程有很多相似之处,但是相对来说又简单了很多。

二 国际化支持

this.initMessageSource();

国际化的源码其实更简单,就是看容器中是否有名为messageSource的bean实例,如果有则直接赋值给AbstractApplicationContext中messageSource的属性。

如果找不到,则通过DelegatingMessageSource实例化一个messageSource实例,放入容器中。

这意味着,我们可以通过配置类,自定义一个messageSource实例:

@Configuration
public class MessageConfig {

    @Bean(name = "messageSource")
    public MessageSource getMessageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setDefaultEncoding("GBK");
        messageSource.addBasenames("message");
        return messageSource;
    }
}

其具体用法为:

    @Override
    public void run(String... args) throws Exception {
        String message = messageSource.getMessage("user.name", null, null, Locale.ENGLISH);
        String messageCn = messageSource.getMessage("user.name", null, null, Locale.CHINA);
        System.out.println(message);
        System.out.println(messageCn);
        System.out.println("完成国际化测试");
    }

其实就是对resource文件夹下面properties文件的一次封装:

三 初始化监听器的多路播放器

this.initApplicationEventMulticaster();

单看代码这个其实也很简单,就是判断IOC容器中是否有applicationEventMulticaster这个实例,如果有,赋值给当前类AbstractApplicationContext的属性——applicationEventMulticaster。如果没有,就new一个多播器实例——SimpleApplicationEventMulticaster,赋值后,再注入到容器中。

protected void initApplicationEventMulticaster() {
        ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
        if (beanFactory.containsLocalBean("applicationEventMulticaster")) {
            this.applicationEventMulticaster = (ApplicationEventMulticaster)beanFactory.getBean("applicationEventMulticaster", ApplicationEventMulticaster.class);
            ......
        } else {
            this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
            beanFactory.registerSingleton("applicationEventMulticaster", this.applicationEventMulticaster);
            ......

这里需要注意的是,这里的多播器其实就是前面Springboot启动类——SpringApplication的run方法中监听器所使用的多播器,只不过前面利用EventPublishingRunListener类包装了一层,同样也是把SimpleApplicationEventMulticaster作为属性放入到了EventPublishingRunListener中:

public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
    private final SpringApplication application;
    private final String[] args;
    private final SimpleApplicationEventMulticaster initialMulticaster;

    public EventPublishingRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
        Iterator var3 = application.getListeners().iterator();
        while(var3.hasNext()) {
            ApplicationListener<?> listener = (ApplicationListener)var3.next();
            this.initialMulticaster.addApplicationListener(listener);
        }

    }

唯一的不同就是EventPublishingRunListener在Springboot启动结束之后,如果不再被引用,会被垃圾回收器回收,但是容器中beanName为applicationEventMulticaster的实例会一直存在,可以在项目启动后,在任意时候从容器中获取。

四 刷新容器

this.onRefresh();

第一部分是针对SpringMVC层面的主题控制,现在项目通常使用前后端分离,所以用处不大。

其提供的自定义功能,也主要是通过自定义beanName——themeSource来实现:

public static ThemeSource initThemeSource(ApplicationContext context) {
        if (context.containsLocalBean("themeSource")) {
            ThemeSource themeSource = (ThemeSource)context.getBean("themeSource", ThemeSource.class);
            ......

但是这一步,还有一个最重要的功能,会启动Spring内置的tomcat容器,并通过while(true)的形式,来接收所有的Servlet请求。

五 注册监听器到IOC容器的多播器

this.registerListeners();

这里的监听器分为两块:

//启动过程中加入的监听器
Iterator var1 = this.getApplicationListeners().iterator();

        while(var1.hasNext()) {
            ApplicationListener<?> listener = (ApplicationListener)var1.next();
            //以监听器的方式加入多播器
            this.getApplicationEventMulticaster().addApplicationListener(listener);
        }
        //实现接口ApplicationListener的监听器,多用于自定义监听器
        String[] listenerBeanNames = this.getBeanNamesForType(ApplicationListener.class, true, false);
        String[] var7 = listenerBeanNames;
        int var3 = listenerBeanNames.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            String listenerBeanName = var7[var4];
       //以beanName的方式加入多播器            this.getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
        }

一块是SpringApplication在执行run方法的过程中,不断加入IOC容器中的监听器,相当于系统自带监听器(也可以自定义),另一块是需要从IOC中获取实现了接口ApplicationListener的监听器,我们如果要实现自定义的监听器,可以直接实现这个接口,然后把监听器注入IOC容器,就可以被IOC的多播器来执行了:

public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E var1);
}

注意只是加入了IOC容器,而监听器是要依靠IOC多播器去实现的,由于前面才刚把IOC自己的多播器实例化,所以此时也才能往多播器中添加监听器。

在加入两种类型的监听器以后,利用IOC的多播器执行一个指定集合——earlyApplicationEvents中的所有监听器方法:

Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
        this.earlyApplicationEvents = null;
        if (earlyEventsToProcess != null) {
            Iterator var9 = earlyEventsToProcess.iterator();
            while(var9.hasNext()) {
                ApplicationEvent earlyEvent = (ApplicationEvent)var9.next();
                this.getApplicationEventMulticaster().multicastEvent(earlyEvent);
            }
        

六 完成bean的大规模实例化

this.finishBeanFactoryInitialization(beanFactory);

在开始bean的大规模实例化之前,还需要做几个准备工作。

首先设置IOC中的类型转换器——ConversionService:

 if (beanFactory.containsBean("conversionService") && beanFactory.isTypeMatch("conversionService", ConversionService.class)) {
  beanFactory.setConversionService((ConversionService)beanFactory.getBean("conversionService", ConversionService.class));
}

再判断容器中是否有配置文件解析器,如果没有,利用Environment的文件解析器:

if (!beanFactory.hasEmbeddedValueResolver()) {
    beanFactory.addEmbeddedValueResolver((strVal) -> {
        return this.getEnvironment().resolvePlaceholders(strVal);
    });
}

实例化接口——LoadTimeWeaverAware的实现类:

String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
String[] var3 = weaverAwareNames;
int var4 = weaverAwareNames.length;

for(int var5 = 0; var5 < var4; ++var5) {
	String weaverAwareName = var3[var5];
	this.getBean(weaverAwareName);
}

释放类加载器——tempClassLoader:

beanFactory.setTempClassLoader((ClassLoader)null);

冻结容器中的所有bean定义信息,不允许有新的bean定义信息注册进来,为大规模实例化bean做准备:

beanFactory.freezeConfiguration();

 6.1 大规模实例化bean

beanFactory.preInstantiateSingletons();

由于preInstantiateSingletons这个方法写的实在是有点难以理解,所以这部分代码需要从整体层面来理解,这样就不至于过于纠结细节,以至于完全无法抓住重点。

首先,我们看下这个方法的大概结构: 

可以看到,其主要功能就是获取所有的bean定义信息,构造成一个List,之后用while(true)的形式,无限循环,用以实例化完所有需要实例化的bean,这是这个方法的本质功能。

那么什么时候跳出循环呢?

让我们忽略掉那一层套一层的do...while循环,可以发现在bean定义信息的List迭代完成以后,就会以return的方式,直接结束此次方法的调用。 

这样我们就大概理解了这个方法的作用,和退出方法的时机。

再看一下迭代器具体做了什么?

如果当前beanName对于的bean实例是SmartInitializingSingleton的实现类,则执行其指定的接口方法afterSingletonsInstantiated。

到这一步,又出现更让人迷惑的一个嵌套,第二个while(true)循环是干嘛的?什么时候退出?

依旧是需要忽略一些无关的细节,从整体来看:

可以看到,第二个while(true)循环,跳出的时机是当beanName是FactoryBean的时候,需要跳出当前的while循环,同时也可以看出,如果不是FactoryBean,会直接进行实例化,然后进入当前while(true)循环中的下一次do...while循环,也就是一直进行普通bean的实例化,直到出现FactoryBean,跳出当前while(true)循环。

跳出去之后呢?别急,外面还有一层while(true)循环等着,此时再结合最外层do...while循环中的条件:

} while(!(bean instanceof FactoryBean));

这样do...while方法也跳出去了,FactoryBean类型的bean进入到了最外层while(true)方法的下半部分执行逻辑:

 可以看到,主要是判断当前bean的实例是否是SmartFactoryBean,以及根据它的属性isEagerInit的值,判断是否需要马上实例化beanName。

至此,preInstantiateSingletons方法的大概逻辑就梳理完了,但是有产生了一些新的问题。

第一个问题,这个方法中,除了刚才介绍的最外层的do...while循环,在第二层的while(true)循环里面,那3层do...while又分别是做什么的?

第二个问题,为什么FactoryBean的实例,在前半部分代码中执行了getBean以后,下面如果其isEagerInit属性是true,还要getBean一次,这又是为什么?

6.1.1 连续三层do...while循环作用

先从最外层循环开始,首先会执行最外层do块,什么都不做,进入第二层do块,也什么都没做。

进入第三层do块,获取下一个beanName的bean定义信息,在最内层do块执行完以后,如果当前bean是一个抽象类,继续执行当前最内层do...while循环,获取下一个bean定义信息,如果当前bean不是一个抽象类,跳出最内层do...while。

执行第二层do...while,嗯,这一层什么都没做,接着判断,如果当前bean定义信息不是一个单例模式(是多态或者其他模式),继续执行最内层do...while循环,获取下一个bean定义信息,此时如果当前bean不是一个抽象类,且是单例模式,才能跳出第二层do...while循环,否则就一直在二三层do...while循环之间,抽取下一个bean定义信息,直到beanName的list中所有的信息都被迭代完,才会结束此方法。

如果bean的定义信息既不是抽象类,又是一个单例,那么会跳到第一层do...while方法,再判断如果bean的定义信息不是懒加载的方式,会跳出当前的do...while方法,也就是跳出所有的三层do...while,进入下一步。

否则,就又重新进入第二层do...while,重复上面的方法,获取下一条bean定义信息,直到一个bean既不是抽象类,又是单例,且不是懒加载的定义,才能跳出所有三层do...while循环。

6.1.2 FactoryBean是什么?为什么要执行2次getBean方法?

什么是FactoryBean

FactoryBean又叫做工厂Bean,指用于生产bean的工厂。

这样说还是有点抽象,通过例子来说明,先看一下FactoryBean的接口信息:

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
    @Nullable
    T getObject() throws Exception;
    @Nullable
    Class<?> getObjectType();
    default boolean isSingleton() {
        return true;
    }
}

可以看到,默认是单例,FactoryBean和我们获取普通的bean的区别在于,需要通过getObject方法来获取真正的bean实例。

这也是为什么在方法preInstantiateSingletons中,需要通过两次getBean方法,才能真正获取到FactoryBean类型的实例的原因。

举个例子:

@Component
public class TestFactoryBean implements SmartFactoryBean {
    @Override
    public Object getObject() throws Exception {
        return new Person();
    }
    @Override
    public Class<?> getObjectType() {
        return null;
    }
    @Override
    public boolean isEagerInit() {
        return true;
    }
}

在Spring容器中,会存在两个实例,一个是通过getBean("&testFactoryBean"),生成的TestFactoryBean实例,一个是通过getBean("testFactoryBean"),生成的Person实例,其实就是通过TestFactoryBean实例的getObject方法,生成的Person实例。

这也是为什么如果当前bean定义不是懒加载的化,FactoryBean需要通过两次getBean方法生成的原因。

七 完成IOC刷新

this.finishRefresh();
protected void finishRefresh() {
    //清楚资源缓存
	this.clearResourceCaches();
    //初始化生命周期处理器
	this.initLifecycleProcessor();
    //执行生命周期处理器onRefresh方法
	this.getLifecycleProcessor().onRefresh();
    //发布容器已刷新监听事件
	this.publishEvent((ApplicationEvent)(new ContextRefreshedEvent(this)));
    //把当前ioc容器加入集合————applicationContexts
	LiveBeansView.registerApplicationContext(this);
}

至此在AbstractApplicationContext类中,已经执行完了refresh方法中的所有步骤。

八 完成IOC刷新

在AbstractApplicationContext完成ioc容器刷新后,需要回到类SpringApplication中,继续执行其run方法,接着执行如下方法:

this.afterRefresh(context, applicationArguments);

由于这是一个抽象方法,且没有继承类,略过。

九 结束Springboot启动流程

	//stopWatch记录结束
    stopWatch.stop();
	if (this.logStartupInfo) {
		(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
	}
    
    //容器启动完成监听器方法执行
	listeners.started(context);
    //容器启动后,执行一系列指定Runners
	this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
	this.handleRunFailure(context, var10, exceptionReporters, listeners);
	throw new IllegalStateException(var10);
}

try {
    //容器正在运行监听器方法执行
	listeners.running(context);
    //返回IOC容器,结束方法
	return context;

这里需要注意的是

this.callRunners(context, applicationArguments);

其实就是执行容器中所有的ApplicationRunner和CommandLineRunner类型的run方法,其效果和bean实例化过程中的initializeBean方法中执行接口InitializingBean的afterPropertiesSet,是一样的。

但是其底层的原理完全不一样,一个是bean的实例化过程中执行的,一个是IOC容器启动完成以后回调执行的:

private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        Iterator var4 = (new LinkedHashSet(runners)).iterator();

        while(var4.hasNext()) {
            Object runner = var4.next();
            if (runner instanceof ApplicationRunner) {
                this.callRunner((ApplicationRunner)runner, args);
            }

            if (runner instanceof CommandLineRunner) {
                this.callRunner((CommandLineRunner)runner, args);
            }
        }

    }

十 总结

至此,我们通过四篇文章,十万余字的代码和文字说明,大概完成了Springboot的全部启动流程,完整而全面的分析了Springboot的全流程。

但是在完成的过程中,深感对一些核心知识点的介绍还不够深入,后续会以单点的形式对Springboot启动过程中的一些知识点进行针对性梳理,以便能够更好的理解Spring框架。

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

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

相关文章

04 YAML kubetnetes世界里的通用语

文章目录1. 前言2. 声明式和命令式是怎么回事&#xff1f;3. 什么是YAML&#xff1f;4. 什么是API对象&#xff1f;4.1 k8s都有哪些资源对象4.2 列出kubectl 命令详细执行过程5. 如何描述 API 对象5.1 命令式5.2 声明式5.2.1 声明式YAML语法详解5.2.1.1 header部分详解5.2.1.2 …

【教学类-19-01】20221127《ABAB式-规律排序-A4竖版2份》(中班)

展示效果&#xff1a; 单人使用样式&#xff1a; 单页打印样式 ​ 背景需求&#xff1a; 中班幼儿需要掌握ABAB规律排序&#xff0c;如下图所示&#xff0c;AB两个元素能外形不同、颜色不同。 ​ ​利用Python Word单元格填色功能&#xff0c;随机生成AB样式&#xff0c;引…

STM32模拟IIC与IIC四种实现数字光强采集模块GY30(标准库与HAL库)

目录 代码实现是的IIC通信&#xff0c;数据采集后在串口显示&#xff0c;方便大家实现二次开发 原件选择 GY-30 数字光强度介绍 BH1750芯片参数 引脚说明 BH1750指令集 接线表设计 通过四种方式实现GY-30数据采集 1.标准库模拟IIC实现GY-30采集并串口1显示 2.标准库IIC…

重构uniapp uni-ui coloerUI项目

重构uniapp uni-ui coloerUI项目这里写自定义目录标题重构uniappuni-uicoloerUI项目起源流程重构uniappuni-uicoloerUI项目 起源 从网上复制了若依移动端的代码,但是对里面的文件夹布局方式和第三方组件库引入方式不甚了解,就想着从头创建一个空白项目&#xff0c;然后一步一…

Linux中设置开机启动执行命令和普通用户配置环境变量开机启动生效

记录&#xff1a;343 场景&#xff1a;在CentOS 7.9操作系统上&#xff0c;开机启动就执行自定义的命令&#xff0c;配置rc.local文件达到需求&#xff1b;在普通用户中配置环境变量开机启动生效&#xff0c;使用profile实现。 版本&#xff1a; 操作系统&#xff1a;CentOS…

01、Docker入门

目录 1、Docker是什么 2、Docker与虚拟化 3、Docker虚拟化的好处 好处一&#xff1a;应用部署方便 好处二&#xff1a;服务器同等配置&#xff0c;性能更优&#xff0c;利用率更高 4、核心概念 5、CentOS7 安装docker(在线方式) 6、镜像 7、Docker容器 8、查看Docker容…

typescript 八叉树的简单实现

查了一些文章&#xff0c;准备自己用typescript写一个简单的八叉树场景管理。 所谓的简单&#xff0c;就是所有元素都是边长为1的立方体。 元素类和树节点类 //元素类&#xff0c;因为都是边长为1的立方体&#xff0c;所以就用cube命名 export class CubeData {public reado…

由于没有远程桌面授权服务器可以提供许可证,远程会话连接已断开

一、问题描述 在使用Windows的远程桌面工具连接WindowsServer2016服务器时&#xff0c;无法连接到服务器&#xff0c;并且提示【由于没有远程桌面授权服务器可以提供许可证&#xff0c;远程回来连接已经断开。请跟服务器管理员联系】。 二、解决办法 2.0、前提 Windows Serv…

黑胶歌曲没权限,看我python大展神通,一分钟一个歌单

前言 大家早好、午好、晚好吖 ❤ ~ 人之初&#xff0c;喜白嫖。 大家都喜欢白嫖&#xff0c;我也喜欢&#xff0c;那么今天就来试试怎么白嫖抑云~ 一、需要的准备 1、环境 Python3.6以上 pycharm2019以上 2、模块 requests # 发送请求模块 第三方模块 exec js # 调用JS的…

CocosCreater 教程(下)

1.物理系统 1.1 2D刚体 刚体是组成物理世界的基本对象。 1.2 2D 碰撞组件 目前引擎支持三种不同的碰撞组件&#xff1a; 盒碰撞组件&#xff08;BoxCollider2D&#xff09;、圆形碰撞组件&#xff08;CircleCollider2D&#xff09; 和 多边形碰撞组件&#xff08;PolygonCo…

Java中的抽象类和接口

java中的抽象类和接口抽象类什么是抽象类&#xff1f;抽象的使用场景抽象类的案例抽象类的特征、注意事项小结抽象类的应用知识&#xff1a;模版方法模式接口接口概述、特点接口的基本使用&#xff1a;被实现接口与接口的关系&#xff1a;多继承JDK8开始接口新增方法接口的注意…

AtCoder Beginner Contest 277 F. Sorting a Matrix(拓扑排序+虚点)

题目 n*m(2<n,m<1e6,n*m<1e6)的矩阵&#xff0c; 第i行第j列元素a[i][j](0<a[i][j]<n*m) 对于值为0的元素&#xff0c;你可以将其赋值为任意正整数&#xff0c; 不同位置的0元素&#xff0c;可以被赋值成不同的正整数 然后&#xff0c;你可以执行以下操作若…

firefly3399 移植linux5.15.80 - 2022-11-27

需要注意的是&#xff0c;虚拟机需要足够的硬盘空间&#xff0c;不小于15GB&#xff01;&#xff01; 一、内核源码下载 国内镜像地址 git clone https://kernel.source.codeaurora.cn/pub/scm/linux/kernel/git/stable/linux.git/ 基本达到了带宽的最大值。 国外地址&#…

Android使用AudioTrack播放WAV音频文件

目录 1、wav文件格式 2、wav文件解析 3、wav文件播放 QA&#xff1a; 开始播放wav的时候使用了系统的播放器mediaplayer进行播放&#xff0c;但是无奈mediaplayer支持的实在不好。 好些年前自己做过pcm播放使用的是audiotrack&#xff0c;参考&#xff1a;CSDN 其实两者之…

php 进程池设计与实现,phper必学!

php 进程池设计与实现phper 为什么要学习进程池池的概念为什么要有进程池?动态创建进程缺点进程池的优点选择子进程为新任务服务的方式进程池模型服务端客户端结语phper 为什么要学习进程池 在php开发过程中经常使用的 php-fpm 使用的进程模型就是进程池&#xff0c;学习进程…

如何基于FSM有限状态机实现Enemies AI

文章目录&#x1f35f; Preface&#x1f355; 巡逻状态&#x1f37f; 寻路状态&#x1f32d; 攻击状态&#x1f357; 完整代码&#x1f35f; Preface 本文简单介绍如何基于FSM有限状态机实现Enemies AI&#xff0c;首先定义敌人的AI逻辑&#xff1a;默认状态下Enemy为巡逻状态…

刷爆力扣之等价多米诺骨牌对的数量

刷爆力扣之等价多米诺骨牌对的数量 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&…

使用 nlohmann 解析 json 文件

使用 nlohmann 解析 json 文件nlohmann/json的配置json基本数据结构json文件的读取、构造与输出C对象与nlohmann::json对象的转换C对象转换成nlohmann::json对象nlohmann::json对象转换成C对象序列化反序列化序列化nlohmann 是德国工程师&#xff0c;以其名字为工程名的 nlohm…

springboot项目的打包发布部署,jar和war的区别

简介&#xff1a; 1.Spring Boot使用了内嵌容器&#xff0c;因此它的部署方式也变得非常简单灵活&#xff0c;可以将Spring Boot项目打包成JAR包来独立运行&#xff0c;也可以打包成WAR包部署到Tomcat容器中运行&#xff0c;如果涉及大规模的部署&#xff0c;Jenkins成为最佳选…

【HCIP-Datacom】 IS-IS基础 ISIS动态路由协议配置(ISIS思维导图在底部)

目录 ISIS配置方法&#xff1a; 路由计算&#xff1a; ATT置位条件&#xff1a; 路由渗透&#xff1a; ISIS的认证&#xff1a; ISIS配置命令&#xff1a; ISIS的开销类型&#xff1a; ISIS配置方法&#xff1a; 进入ISIS进程 isis 1 //创建isis进程 设置实体名 network-entit…