SpringBoot项目启动过程源码终于整体捋了一遍(二)

上一篇写到run()方法分两步,即:

1.new SpringApplication() 利用主源类primarySources初始化SpringApplication类

2.调用SpringApplication的run()方法启动项目,并传递参数args。

也提到启动的时候主源类primarySources可以有多个,那就有一个问题了,这个primarySources为什么还可以有多个呢,启动的时候需要从主源类中获取什么信息,也就是primarySources有什么用,这篇就带着这个问题继续往下撸源码,先看上面提到的两步中的第一步,即new SpringApplication()初始化SpringApplication类:

 SpringApplication类有两个构造方法,区别就是是否指定资源加载器resourceLoader,如果不指定的话就将其赋值为null,上一篇的例子中启动类中直接使用SpringApplication.run(),所以默认调用的第一种构造器没有指定resourceLoader,可见SpringBoot启动还可以先自己初始化SpringApplication并指定resourceLoader,然后在启动。

那这个resourceLoader又是什么东西呢?看第二个构造器的第一行:

上来就给this.resourceLoader赋值,然后直接在该类中Crtl+F搜索这个this.resourceLoader,首先看到这个:

原来即使构造SpringApplication的时候不指定 resourceLoader也没关系,还提供了setResourceLoader()方法设置,为时不晚,继续搜索,看到这里:

上面那个方法就不说了,提供方法拿到当前的resourceLoader,重点是下面那个方法里的this.resourceLoader.getClassLoader(),该类中其他地方用到这个resourceLoader也都是这样,其实看名字也就明白了,原来这个resourceLoader就是为了拿到类加载器。之所以可以自己设置启动时的resourceLoader也是为了可以应用自定义类加载器之类的吧,那看到这里又有一个疑问了,我不传resourceLoader就给它赋值为null的啊,类加载器还能为null?这不闹呢嘛,这个问题先留着,源码继续往下撸会解释resourceLoader为null的时候怎么办。

继续看构造方法的第二行:

这就不用说了,没有primarySources会报错,这更加让我疑惑这个primarySources有什么用,可以有多个,没有就报错。这里的Assert类可以学习用一下,可以优雅地抛出异常。

继续看第三行:

呦吼终于出现了,赶紧Crtl+F一下这个this.primarySources,看哪里用到了,首先看到这个:

居然还带半路添加primarySources的,继续搜索,看到这里:

没有其他地方用到这个this.primarySources了,这个getAllsource()方法就是获取所有的primarySources,看来关键在于哪里调用了这个getAllSources(),一共也就两个类中会调用这个方法,其中一个就是SpringApplication类本身,另一个暂时不管,SpringApplication类中prepareContext()方法中调用了getAllSources():

 调用getAllSources()无非是为了拿到所有的primarySources,另一个调用这个方法只是为了判断prepareContext是否为空,所以没必要管,而这个prepareContext()需要看一看。

文章开头说的两步,第二步run()方法中会调用这个prepareContext()方法,这个方法是为了初始化Spring上下文Applicationcontext,具体的暂时先不做介绍,因为后续的文章迟早会撸到这一步,到时候再说,先直接看调用getAllSource()的这几步,即:

源码中还特意用注释与前面隔开了,因为和前面确实没什么关系 ,这几步是为了加载拿到的所有的primarySources,那就看load()方法:

 先看getBeanDefinitionRegistry(context),可知这个上下文context的作用是拿到一个Bean定义的注册器,可以看看这个getBeanDefinitionRegistry()方法:

方法里没什么,需要好好理理这几个类之间的继承关系,就不难理解了,这里就不理了怕扯远,就当这个context厉害得很,就是能拿到这个BeanDefinitionRegistry注册器,然后重点应该是createBeanDefinitionLoader()方法,有了注册器和需要被加载的资源,就可以先拿Bean定义的加载器了即BeanDefinitionLoader,方法里是这样的:

 就是用注册器和source构造了一个BeanDefinitionLoader,那就看看这个构造方法:

 意思就是注册器和被加载的资源source不能为空,然后初始化了source,等着被加载吧。还初始化了几种Bean定义的读取器,annotateReader即注解形式的Bean定义读取器,xmlReader即XML形式的Bean定义读取器,还有一个Grooyy语言的定义读取器。现在知道为什么代码中既可以用注解@Bean、@Service这些去将对象交给Bean容器管理,又可以在XML中配置Bean,就是这两个读取器在干活。还有一个Grooyy语言不常用就不说了。scanner是类路径扫描器,可以指定类的路径去扫描读取,这也算是一种读取器,最后是给这个类路径扫描器添加了排除过滤器,还可以排除某些路径不去那里读取,具体细节可以去看看ClassPathBeanDefinitionScanner这个类,这里就不展开了。

然后往上翻倒数第四张图,再回到这里,为了看图方便那张图再贴一遍:

已经拿到这个Bean定义的加载器BeanDefinitionLoader了,然后是一顿set,其中有一个setResourceLoader(),这不是set之前说的那个类加载器嘛,这里告诉我们不要把这个Bean定义加载器和类加载混淆,它还是需要类加载器的。另外两个set的东西也介绍一下,BeanNameGenerator主要功能是从一定的条件中计算出bean的name,bean容器的管理很大一部分是基于beanName的,了解这个可以让我们自定义beanName的生成规则。而这个this.environment即ConfigurationEnvironment应用运行时环境,常见的用途就是用来根据键获取配置文件的值,示例如下:

yml配置文件里这样就能获取到值了:

ConfigurationEnvironment类还有很多其他方法,毕竟运行时环境中的信息还是挺多的,这些可以去了解一下。然后终于到loader.load()这一步了,看一下这个load()方法:

大佬们都喜欢重载方法,刚loader.load()调用的是第一个load()方法,里面就是遍历调用第二个load()方法,并且计数。第二个load()方法里将被加载的资源分类型,一共是四种类型,又来四个load()。

第一种是加载Class,调用了这个load()方法:

一上来又是判断是否是Groovy语言,这个不管,然后有一个isComponent()方法判断资源是否为组件,判断成功了才用之前说的注解形式的Bean定义读取器去注册资源,那就看一下这个isComponent()方法:

如果打上了@Component注解直接返回true,如果没有这个注解,依次为如果是内部类、匿名类或者没有构造函数的类,就返回false。这里要注意一下,避免开发过程中有些对象不能被Bean容器管理。

如果这个isComponent()方法返回true,下一步就是用之前提到的注解形式Bean定义读取器加载资源,即this.annotatedReader.register()。

第二种是加载Resource,调用了这个load()方法:

忽略Groovy的话就是用之前说的XML形式Bean定义读取器去加载资源。

第三种是加载Package,调用了这个load()方法:

就是使用包扫描器加载资源。

第四种是加载CharSequence,调用的load()方法有点长就不贴了,这种形式实际上等同于source为字符串,解析字符串后进行匹配上面三种方式,即Class、Resource和Package,哪个匹配上了就用哪种方式去加载。

看到这几种方式就可以联想到前面构造了BeanDefinitionLoader的时候,初始化的就是这几个东西。

源码撸到这里已经离SpringBoot启动流程这条主线有点远了,其实一直都在讲Bean加载的过程,也就是主源类primarySources的加载,Bean加载就Bean加载吧,继续往下看,这四种加载方式除去第四种是匹配前三种加载方式,也就前三种加载Bean的方式我们最常用的应该是第一种和第三种,第一种更多,那就着中看看第一种加载Class的方式是怎么加载Bean的吧,还是那张图再贴一遍:

 得看看this.annotateReader.register()方法里有什么:

就这?又是一个遍历注册,看看registerBean()方法:

调用了doRegisterBean()方法,并且其他三个参数都为null,看一下这个方法:

这个方法的第二个参数instanceSupplier是创建Bean实例的回调,可见SpringBoot也提供了自定义注册Bean的方法,那就一行一行来分析吧。

首先上来就是利用bean的class构造了一个AnnotatedGenericBeanDefinition对象,暂且和源码中一样称之为abd,看一下这个对象的构造方法:

 AnnotatedGenericBeanDefinition实现了接口AnnotatedBeanDefinition,而AnnotatedBeanDefinition继承了BeanDefinition。Spring中使用BeanDefinition描述了一个bean的实例,而这个AnnotatedBeanDefinition比BeanDefinition还多了两个获取类上注解的方法:

那这个abd岂不是厉害了,即能描述一个bean实例即获取bean的各种信息比如beanName、scope什么的,又能获取类上的注解 ,留着有用。

继续往下看:

 嗯哼?看着意思还有不注册这个bean的情况?那就看一看shouldSkip()方法,截取一部分:

原来@Conditional注解作用在这里,@Conditional 是Spring 4框架的新特性。此注解使得只有在特定条件满足时才启用一些配置,也就是注册这个bean,如果没有这个注解直接返回false,也就是继续注册,有这个注解后面一长串是否满足注解中的条件。

继续往下看:

 setInstanceSupplier()是设置bean注册的回调,就是给这个abd设置的,其实这个注册bean的过程很大一部分时间都在给abd各种set属性,看来这个abd很有用。然后又是拿到注解scope的值set给abd,如果没有@scope注解则默认为单例"singleton"。scope有哪几种值接下来就会说到。

继续往下看:

原来beanName是这时候出来的,beanNameGenerator前面也提到了,generateBeanName()方法里面有一定的规则去生成beanName。规则大致介绍下,图就不贴了,大概就是获得所有注解,看是不是有@Component、@ManageBean或者是@Name注解,有任何一个就将他们的value值返回作为beanName,如果都没有配置value,就会返回默认的baenName,这就和我们平时的使用对上了,而且最后还会判断value的值不能多于一个,举个例子就是不能在一个类上打了@Component又打@Service而且都定义了value,不然会报错,详细的可以点进方法看。

继续往下看:

这是个什么鬼?看方法名意思是处理通用注解?点进去看看:

 害,确实是处理了5个注解,不过说到底又是给abd的一顿set,这五个注解分别是:

@lazy注解:用于指定该Bean是否懒加载,如果该注解的value为true的话,则这个bean在spring容器初始化之后,第一次使用时才初始化。AbstractBeanDefinition中定义该值的默认值是false。
@primary注解:自动装配时当出现多个Bean候选者时,被注解为@Primary的Bean将作为首选者,否则将抛出异常。
@DependsOn注解:定义Bean初始化顺序。如果这个bean是AbstractBeanDefinition的子类的话,还会处理以下两个注解:
@Role注解:用于用户自定义的bean,value值是int类型,表明该bean在应用中的角色,默认值是0。
@Description注解:用于描述bean,提高代码可读性。

继续往下看:

上面刚除了完了5个注解,这又来处理@Qualifier注解给abd一顿set,@Autowired是根据类型进行自动装配的。如果当Spring上下文中存在不止一个该类型的bean时,就会抛出BeanCreationException异常。@Qualifier可以配置自动依赖注入装配的限定条件,@Qualifier 可以直接指定注入 Bean 的名称,简单来说, @Autowired和@Qualifier 结合使用时,自动注入的策略就从 byType 转变成 byName 了。

继续往下看:

这段代码是spring5.0以后新加入的,Spring 5允许使用lambda 表达式来自定义注册一个 bean。

继续往下看:

这个abd都被set这么肥了,终于用到它了,可以把这个BeanDefinitionHolder理解为abd的一个持有者,先拿着吧,以后想用abd就直接找这个definitionHolder就行了。重点关注一下applyScopedProxyMode()方法:

这段代码首先需要获得ScopedProxyMode值,这个值实在@scope注解中使用proxyMode属性设置的,默认为NO,就是没有代理。它还可被设置为ScopedProxyMode. INTERFACES或者是ScopedProxyMode. TARGET_CLASS。这个值具体有什么用呢,我们知道@scope注解的value可以设置为以下几种:
单例(singleton):在整个应用中,只创建bean的一个实例。
原型(prototype):每次注入或者通过Spring应用上下文获取的时候,都会创建一个新的实例。
会话(session):在Web应用中,为每个会话创建一个bean实例。
请求(request):在Web应用中,为每个请求创建一个bean实例。
当一个singleton作用域的bean中需要注入一个session作用域的bean的时候,会报错,应为此时应用没有人访问,session作用域bean没有创建,所以出现了问题。spring提供给我们的解决方案就是通过设置proxyMode属性的值来解决,当他设置为ScopedProxyMode. INTERFACES或者是ScopedProxyMode. TARGET_CLASS时,spring会为session作用域bean创建代理对象,而真正调用的bean则在运行时懒加载,两者的区别是一个使用JDK提供的动态代理实现,一个使用CGLIB实现。
这段代码就是通过判断proxyMode的值为注册的Bean创建相应模式的代理对象。默认不创建。

终于到最后一行了,就是这个registerBeanDefinition()方法,墨迹了这么久终于开始注册bean了?,看一下这个方法:

拿到beanName又掉了一个registerBeanDefinition()方法,后面aliases别名就不管了,这些方法名一样的方法只要理清类直接的集成关系,就很容易找到到底调用的哪里的方法,看一下这个registerBeanDefinition()方法:

@Override
	public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
			throws BeanDefinitionStoreException {

		Assert.hasText(beanName, "Bean name must not be empty");
		Assert.notNull(beanDefinition, "BeanDefinition must not be null");

		if (beanDefinition instanceof AbstractBeanDefinition) {
			try {
				((AbstractBeanDefinition) beanDefinition).validate();
			}
			catch (BeanDefinitionValidationException ex) {
				throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
						"Validation of bean definition failed", ex);
			}
		}

		BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
		if (existingDefinition != null) {
			if (!isAllowBeanDefinitionOverriding()) {
				throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
			}
			else if (existingDefinition.getRole() < beanDefinition.getRole()) {
				// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
				if (logger.isInfoEnabled()) {
					logger.info("Overriding user-defined bean definition for bean '" + beanName +
							"' with a framework-generated bean definition: replacing [" +
							existingDefinition + "] with [" + beanDefinition + "]");
				}
			}
			else if (!beanDefinition.equals(existingDefinition)) {
				if (logger.isDebugEnabled()) {
					logger.debug("Overriding bean definition for bean '" + beanName +
							"' with a different definition: replacing [" + existingDefinition +
							"] with [" + beanDefinition + "]");
				}
			}
			else {
				if (logger.isTraceEnabled()) {
					logger.trace("Overriding bean definition for bean '" + beanName +
							"' with an equivalent definition: replacing [" + existingDefinition +
							"] with [" + beanDefinition + "]");
				}
			}
			this.beanDefinitionMap.put(beanName, beanDefinition);
		}
		else {
			if (hasBeanCreationStarted()) {
				// Cannot modify startup-time collection elements anymore (for stable iteration)
				synchronized (this.beanDefinitionMap) {
					this.beanDefinitionMap.put(beanName, beanDefinition);
					List<String> updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1);
					updatedDefinitions.addAll(this.beanDefinitionNames);
					updatedDefinitions.add(beanName);
					this.beanDefinitionNames = updatedDefinitions;
					removeManualSingletonName(beanName);
				}
			}
			else {
				// Still in startup registration phase
				this.beanDefinitionMap.put(beanName, beanDefinition);
				this.beanDefinitionNames.add(beanName);
				removeManualSingletonName(beanName);
			}
			this.frozenBeanDefinitionNames = null;
		}

		if (existingDefinition != null || containsSingleton(beanName)) {
			resetBeanDefinition(beanName);
		}
	}

方法全贴了,但是我实在不想仔细看了,不过一眼就看到了这句:

行吧行吧,注册好了原来就是以beanName为key放在了一个Map里面,value就是之前一直在set的abd。

其实这还远不是bean加载的过程,只能理解为beanDefinition的加载过程,因为最终产物是能描述bean信息的beanDefinition,至于这个beanDefinition的作用前面也提到了,bean加载还有后续很多步骤,这里甚至连bean的对象都没有构造出来,更别说属性注入、创建代理对象这些了。

这是我写过的最长的一篇文章了,可是对最初的主线Spring Boot启动流程而言仅仅是撸了一行代码,就为了看那个主源类primarySources有什么用,就这么朝bean加载发散出去了,现在知道primarySources的beanDefinition会被放进一个map,然后就可以根据SpringApplication的getAllSources()方法获取所有的primarySources,然后再从这个map里中获取primarySources的实例信息,注解信息什么的,应该这样吧,也是猜的。

依然总结一下:这篇文章一直是在分析SpringApplication类的构造方法,对于Spring Boot启动流程就走了三行代码,但是摸了一下Bean加载的过程,也就是如何加载主源类primarySources。而且还留了一个问题,就是初始化SpringApplication的时候resourceLoader为null的时候怎么拿类加载器。

发布了76 篇原创文章 · 获赞 57 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/weixin_42447959/article/details/104943589