SpringBoot- 技术专题 - 启动自动装配过程

出发:
个人觉得,并非应该从,@SpringBootApplication 这个注解讲起,因为如果构建扫描机制和容器,这个复合注解根本不会被 spring 框架所接受和识别对吧!。

进入到 SpringApplication 这个类中看一下 run()方法的核心实现。

SpringApplication.run()方法中,主要做了两件事情:
第一个就是创建 ApplicationContext 容器。
第二个是刷新 ApplicationContext 容器。
在创建 ApplicationContext 时,会根据用户是否明确设置了 ApplicationContextClass 类型以及初始化阶段的推断结果,决定为当前 SpringBoot 应用创建什么类型的 ApplicationContext。

创建完成 ApplicationContext 容器后,我们接着回到 SpringApplication.run()方法中。 下面开始初始化各种插件在异常失败后给出的提示。 然后执行准备刷新上下文的一些操作。其实 prepareContext()方法也是非常关键的,它起到了一个承上启下的作用。下面我们来看一下 prepareContext()方法里面具体执行了什么。

主要就是 getAllSources()方法,这个方法中,获取到的一个 source 就是启动类 DemoApplication。

这样就通过获取这个启动类就可以在后 load()方法中去加载这个启动类到容器中。
后面再通过 listeners.contextLoaded(context); 将所有监听器加载到 ApplicationContext 容器中。
最后是核心的第二部刷新 ApplicationContext 容器操作,如果没有这一步操作上面的内容也都白做的,通过 SpringApplication 的 refreshContext(context)方法完成最后一道工序将启动类上的注解配置,刷新到当前运行的容器环境中。
1.1 启动类上的注解
上面我们说到在 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"
第一个端核心代码意思是: 启动的时候会扫描所有 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 并进行实例化,如果没有配置,默认也是 true
这种 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
到站:
总结:Springboot 启动时,是依靠启动类的 main 方法来进行启动的,而 main 方法中执行的是 SpringApplication.run()方法,而 SpringApplication.run()方法中会创建 spring 的容器且刷新容器。
刷新容器的时候就会去解析启动类,然后就会去解析启动类上的 @SpringBootApplication 注解,而这个注解是个复合注解,这个注解中有一个 @EnableAutoConfiguration 注解,这个注解就是开启自动配置,这个注解中又有 @Import 注解引入了一个 AutoConfigurationImportSelector 这个类,这个类会进过一些核心方法。
然后去扫描我们所有 jar 包下的 META-INF 下的 spring.factories 文件,而从这个配置文件中取找 key 为 EnableAutoConfiguration 类的全路径的值下面的所有配置都加载,这些配置里面都是有条件注解的,然后这些条件注解会根据你当前的项目依赖的 jar 包以及是否配置了符合这些条件注解的配置来进行装载的。
补票:
SpringBoot 在启动的时候会调用 run()方法,run()方法会刷新容器,刷新容器的时候,会扫描 classpath 下面的的包中 META-INF/spring.factories(118)文件,在这个文件中记录了好多的自动配置类,在刷新容器的时候会将这些自动配置类加载到容器中,然后在根据这些配置类中的条件注解,来判断是否将这些配置类在容器中进行实例化,这些条件主要是判断项目是否有相关 jar 包或是否引入了相关的 bean。这样 springboot 就帮助我们完成了自动装配。
评论