前言
继续总结吧,没有面试就继续夯实自己的基础,前阵子的在面试过程中遇到的各种问题陆陆续续都会总结出来分享给大家,这次要说的也是面试中被问到的一个高频的问题,我当时其实没答好,因为很早之前是看过springboot启动过程的源码,但是时间隔得有点久了(两年多没用过springboot),所以当时也没答好。这次好好总结这部分知识。
SpringApplication.run()
我看网上好多介绍springboot自动装配过的文章时,上来就直接说@SpringBootApplication
注解是一个复合注解,从这个注解开始介绍springboot是如何将配置项进行加载的。其实我觉得难道不应该是先启动了spring的容器,然后才能扫到注解,然后才能解析注解吗?也可能是大家觉得创建容器刷新容器这些基础操作都默认知道的,所以就都没说。
但我在分析springboot自动装配的时候,要先从SpringApplication.run()
方法开始。
我们进入到SpringApplication
这个类中看一下run()
方法的核心实现,差不多每一行我都加上了注释了。
SpringApplication.run()
方法中,我把关键点用序号标识出来了。
- 第一个就是创建ApplicationContext容器。
- 第二个是刷新ApplicationContext容器。
在创建ApplicationContext时,会根据用户是否明确设置了ApplicationContextClass
类型以及初始化阶段的推断结果,决定为当前SpringBoot应用创建什么类型的ApplicationContext。
创建完成ApplicationContext容器后,我们接着回到SpringApplication.run()
方法中。
下面开始初始化各种插件在异常失败后给出的提示。
然后执行准备刷新上下文的一些操作。其实prepareContext()
方法也是非常关键的,它起到了一个承上启下的作用。下面我们来看一下prepareContext()
方法里面具体执行了什么。
关键的地方我也标注出来了,主要就是getAllSoures()
方法,这个方法中,获取到的一个source就是启动类DemoApplication。
这样就通过获取这个启动类就可以在后load()方法中取加载这个启动类到容器中。
然后,后面再通过listeners.contextLoaded(context)
;
将所有监听器加载到ApplicationContext容器中。
最后就是我们上面说的核心的第二部刷新ApplicationContext容器操作,如果没有这一步操作上面的内容也都白做的,通过SpringApplication的refreshContext(context)
方法完成最后一道工序将启动类上的注解配置,刷新到当前运行的容器环境中。
启动类上的注解
上面我们说到在SpringApplication的run()
方法中,通过调用自己的prepareContext()
方法,在prepareContext()
方法中又调用getAllSources()
方法,然后去获取启动类,然后通过SpringApplication的load()
方法,去加载启动类,然后在刷新容器的时候就会去将启动类在容器中进行实例化。
在刷新ApplicationContext容器时,就开始解析启动类上的注解了。
启动类DemoApplication
就只有一个注解@SpringBootApplication
,那么下面来看一下这个注解:
可以看到这个注解是一个复合注解,有三个关键注解需要说明一下。
@SpringBootConfiguration
@SpringBootConfiguration
这个注解说明再点进去查看详情发现就是一个@Configuration
注解,这说明启动类就是一个配置类。支持Spring以JavaConfig的形式启动。
@ComponentScan
这个注解,从字面的意思上也能看出来,就是组件扫描的意思,即默认扫描当前package以及其子包下面的spring的注解,例如:@Controller
、@Service
、@Component
等等注解。
@EnableAutoConfiguration
@EnableAutoConfiguration
这个注解也是一个复合注解:
这个注解是比较核心的一个注解,springboot的主要自动配置原理基本上都来自@EnableAutoConfiguration这个注解的配置,那么我们通过看这个注解的源码可以发现有两个注解比较重要的。
- 一个是
@AutoConfigurationPackage
,自动配置包。 - 另一个是
@Import(AutoConfigurationImportSelector.class)
,自动引入组件。
@AutoConfigurationPackage
这个注解字面的意思是自动配置包,那么我们点进去看看里面是什么样的。
还是一个复合注解,但是最终依赖的确实@Import
这个注解,这个注解后面我们会介绍,现在先明白它就是给Spring容器引入组件的功能的一个注解。
那么我们接着来看看AutoConfigurationPackages.Registrar.class
这个类里面的代码。
这两张图就是这个AutoConfigurationPackages.Registrar
这个类的关键部分,说实话,我是没看出来什么东西。但是网上搜到的是这个register()方法的作用是,用来自动注册一些组件中的配置,例如JPA的@Entity
这个注解,这里就是会开启自动扫描这类注解的功能。
@Import(AutoConfigurationImportSelector.class)
我们接着回来看@EnableAutoConfiguration
下的@Import(AutoConfigurationImportSelector.class)
这个注解的功能。进入到AutoConfigurationImportSelector
这个类里面后源码如下:
然后我们进入getAutoConfigurationEntry()
方法来看看:
我们继续进入getCandidateConfigurations()
方法:
看来最核心的方法是SpringFactroiesLoader.loadFactoryNames()
方法了,我们再进入看看:
包的好深,居然还有一层,那么继续进入loadSpringFactories()
方法。
终于到最后一层了,算是“拨开云雾见天日,守得云开见月明”,下面就来梳理一下loadSpringFactories()方法。
首先FACTORIES_RESOURCE_LOCATION
这个常量的值是:
"META-INF/spring.factories"
/**
* The location to look for factories.
* <p>Can be present in multiple JAR files.
*/
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
所以第一个端核心代码的意思是:
启动的时候会扫描所有jar包下META-INF/spring.factories
这个文件。第二段代码的意思是将这些扫描到的文件转成Properties对象,后面两个核心代码的意思就是说将加载到的Properties对象放入到缓存中。
然后getCandidateConfigurations()
方法,是只获取了key是EnableAutoConfiguration.class
的配置。
我们看到getCandidateConfigurations()
方法,通过SpringFactoriesLoader.loadFactoryNames()
获取到了118个配置。
那么我们来看一个spring.factories
文件中的内容是什么样子的呢?
原来是这种形式的,看来这和上一篇文章中讲解的Java中的SPI机制加载接口实现很像啊,其实通过查阅资料发现,这就是一种自定义SPI的实现方式的功能。
那么我们以第一个配置类:
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
来看一下,这些类都是如果实现的。
打开org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
的源码:
我们看到这个类有三个注解@Configuration
、@AutoConfigureAfter
、@ConditionalOnProperty
、因为有@Configuration
注解所以它也是一个配置类,然后第二注解中的参数类JmxAutoConfiguration.class
进入之后是这样的:
也是存在@ConditionalOnProperty
注解的。那看来关键点就是@ConditionalOnProperty
这个注解了。
这个注解其实是一个条件判断注解,这个条件注解后面的参数的意思是当存在系统属性前缀为spring.application.admin
,并且属性名称为enabled
,并且值为true
时,才加载当前这个Bean并进行实例化。
这种spring4.0后面出现的的条件注解,可以极大的增加了框架的灵活性和扩展性,可以保证很多组件可以通过后期配置,而且阅读源码的人,通过这些注解就能明白在什么情况下才会实例化当前Bean。
后面还有不少这种条件注解呢:
@ConditionalOnBean:当容器里有指定Bean的条件下
@ConditionalOnClass:当类路径下有指定的类的条件下
@ConditionalOnExpression:基于SpEL表达式为true的时候作为判断条件才去实例化
@ConditionalOnJava:基于JVM版本作为判断条件
@ConditionalOnJndi:在JNDI存在的条件下查找指定的位置
@ConditionalOnMissingBean:当容器里没有指定Bean的情况下
@ConditionalOnMissingClass:当容器里没有指定类的情况下
@ConditionalOnWebApplication:当前项目时Web项目的条件下
@ConditionalOnNotWebApplication:当前项目不是Web项目的条件下
@ConditionalOnProperty:指定的属性是否有指定的值
@ConditionalOnResource:类路径是否有指定的值
@ConditionalOnOnSingleCandidate:当指定Bean在容器中只有一个,或者有多个但是指定首选的Bean
这些注解其实都是通过@Conditional注解扩展而来的,只是使用了不同的组合条件来判断是否需要加载和初始化当前Bean。
总结
好了,最后总结一下,当面试官问springboot的自动装配原理的时候,不能这么长篇大论的说吧,毕竟这么多内容也记不住啊。
所以总结:
springboot启动时,是依靠启动类的main方法来进行启动的,而main方法中执行的是SpringApplication.run()
方法,而SpringApplication.run()
方法中会创建spring的容器,并且刷新容器。而在刷新容器的时候就会去解析启动类,然后就会去解析启动类上的@SpringBootApplication
注解,而这个注解是个复合注解,这个注解中有一个@EnableAutoConfiguration
注解,这个注解就是开启自动配置,这个注解中又有@Import
注解引入了一个AutoConfigurationImportSelector
这个类,这个类会进过一些核心方法,然后去扫描我们所有jar包下的META-INF
下的spring.factories
文件,而从这个配置文件中取找key为EnableAutoConfiguration
类的全路径的值下面的所有配置都加载,这些配置里面都是有条件注解的,然后这些条件注解会根据你当前的项目依赖的jar包以及是否配置了符合这些条件注解的配置来进行装载的。
这就是springboot自动配置的过程。
其实上面这些内容还是有点多,而且还有好多注解的单词也不好记,那换成大白话,再精炼一下:
SpringBoot在启动的时候会调用run()方法,run()方法会刷新容器,刷新容器的时候,会扫描classpath下面的的包中META-INF/spring.factories文件,在这个文件中记录了好多的自动配置类,在刷新容器的时候会将这些自动配置类加载到容器中,然后在根据这些配置类中的条件注解,来判断是否将这些配置类在容器中进行实例化,这些条件主要是判断项目是否有相关jar包或是否引入了相关的bean。这样springboot就帮助我们完成了自动装配。