SpringBoot的自动装配与Spring框架的注册过程
文章目录
前言
最近看了雷丰阳老师的SpringBoot的课程,之前也学了下Spring的源码,就打算简单去看下SpringBoot自动装配的实现,这个算是我的学习笔记,同时也给大家分享下自己的学习成果。
关键点
- 配置类和自动配置类的定位
- SpringBoot和Spring之间是如何连接的
- ConfigurationClassPostProcessor的工作流程
- ConfigurationClass
- 解析ConfigurationClass的过程
- 处理配置类和注册组件
- Spring框架如何使用SpringBoot中的实现类来完成自动装配
正文
主程序
让我们先看SpringBoot应用的主程序
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
}
}
这里有两个重点
- @SpringBootApplication 注解
- SpringApplication.run(MainApplication.class, args);
让我们先看@SpringBootApplication
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
它是三个注解的集合
- SpringBootConfiguration,它只是简单的被注解了@Configuration
- EnableAutoConfiguration,它是我们要研究的重点,也是自动装配的关键
- ComponentScan,它附带了一些过滤器
在这里插一嘴,大家是否有一个疑问,为什么给注解类注解就能生效,这是JAVA注解本身的特性么?
其实JAVA注解本身是没有这个特性的,一个注解给另一个注解进行注解并不是继承,你也不能简单地从MainApplication中取得@SpringBootConfiguration,只能先取得@SpringBootApplication再去取得@SpringBootConfiguration,注解始终是在为被注解对象提供额外的静态信息。
我们可以像上面那样使用是因为,Spring中的StandardAnnotationMetadata会帮你默认合并所有注解。
@EnableAutoConfiguration
它是我们研究的重点,主要有两个注解:
- @AutoConfigurationPackage
- @Import(AutoConfigurationImportSelector.class)
@AutoConfigurationPackage
简单阅读源码,或者听雷神的课就知道,它在导入当前目录下的组件并注册
@Import(AutoConfigurationImportSelector.class)
这里的AutoConfigurationImportSelector就是重点,目前我们会简单地阅读下源码,之后就会发现它的重要性
AutoConfigurationImportSelector.java
// 这是它的核心方法
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
SpringFactoriesLoader.java
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
我给出的四个方法就是它的核心方法栈,取得了SpringBoot默认配置的127个自动配置类,这些内容也是雷神讲过的,我也就不多讲了,大致就是取得/META-INF/spring.factories中配置的自动配置类,然后SpringBootCondition根据@Conditional注解过滤掉不要的自动配置类(这一块雷神没讲,我也还没有细看,可以去看OnBeanCondition等类,我扫了一眼蛮简单的,之后看了我会补充),再把这些自动配置类进行配置。
然后让我们整理一下,自动配置类,starter,pom.xml之间的关系
- 首先pom.xml导入了starter,starter的maven项目中导入我们需要的依赖
- 我们导入了所有的自动配置类,ConditionOnBean等会判断1中的依赖是否存在来过滤
以上就是我们宏观上使用SpringBoot的效果
SpringApplication.run()
我们基本看了注解给我们提供的信息,但是注解终归只是信息,我们要看看它是如何生效的。
首先稍微翻一下源码,看到run方法除了提供一些很基础的准备后,关键还是
refreshContext(context),这个方法其实就是在调用Spring容器中最关键的方法refresh()
这时候,SpringBoot就已经和Spring框架连接起来了,甚至,其他事情全部都是Spring框架做的。
以下就是Spring容器启动的流程的源码
AbstractApplicationContext.java
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
整个流程中,我们最关注的点应该是:
invokeBeanFactoryPostProcessors,它执行了BeanFactoryPostProcessor的方法。
registerBeanPostProcessors,它注册了BeanPostProcessor,用于后续Bean的实例化各个阶段提供加强接口
finishBeanFactoryInitialization,它是具体的Bean的实例化过程,给不需要延迟加载的直接实例化(一般的单例),给需要的添加对应的工厂(一般的原型,或者懒加载的单例),总体就是根据依赖关系按序调用getBean方法
这三个方法是最重要的,其他的不是对环境准备或者就是暂时留出钩子方法,没有过多的实现。
我们都知道Spring容器中需要先注册BeanDefinition,才能具体实例化,那么我们用@Bean, @Component注解的类是何时被注册的呢?
再在这里插一句,我们在分析自动装配,但为什么我们那么在意@Bean,@Component,那么在意注册的问题?
首先让我们分析一下自动配置类,以WebMvcAutoConfiguration为例
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
}
首先看它的注解,重点肯定是@Configuration,@Conditional,自动配置的顺序就不是我们的重点了,我们可以知道
- @Configuration被注解了@Component
- 我们可以发现@Conditional 其实是spring框架的注解,而不是springboot的,其@ConditionalOnXxx才是
- 有些类其实还会导入其他配置类@Import
- 我们的主配置类也用了@ComponentScan
再看它的内容
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
它提供了
- @Bean方法
- 内部配置类
- 属性配置
从上面的信息可以看出,自动配置类给我们提供了这些信息:
- @Configuration, 当前类是个配置类
- @ComponentScan, 要扫描导入的包
- @Bean, 要导入的组件
- @EnableConfigurationProperties, 自动配置的属性
- @Import, 要导入的类
- @Conditional, 是否导入的条件判断
- 内部配置类
- 其中除了@EnableConfigurationProperties,其他都是跟Spring框架直接相关的。
我们可以知道,自动配置类就是一个配置类,它做的事情就是帮我们导入组件并配置属性,导入组件就意味着它会被注册。
自动配置类其实只是Spring官方等第三方为我们编写的配置类罢了,跟我们自己写的配置类无异。
看到这里,有没有觉得配置就像一个树的节点,其他组件就是树的叶子,@Import,@Bean,@ComponentScan等就是分支,引入新节点或是叶子
我们发现主程序也是一个配置类,它会作为根节点,负责导入所有组件,只要我们处理好了这个树状的结构,我们也就完成了自动装配。
接下来,我们会进入一个新的阶段。
ConfigurationClassPostProcessor
ConfigurationClassPostProcessor是一个BeanFactoryPostProcessor,在Spring容器中invokeBeanFactoryPostProcessors阶段被调用,它会解析配置类并注册所有组件,是Spring最重要的东西之一。
在讲它之前,我们先把刚才的配置类进行抽象,把每个配置类变成一个对象,它对应的类就是ConfigurationClass
final class ConfigurationClass {
// 这是它的注解信息,我之前也说过,它会合并所有层级的注解,放到一个Map里
// 主要包含: @Configuration @Import @Conditional @EnableConfigurationProperties
private final AnnotationMetadata metadata;
// 资源,可以是类路径文件,等等
private final Resource resource;
// 该配置类对应的beanName
@Nullable
private String beanName;
// 导入它的配置类
private final Set<ConfigurationClass> importedBy = new LinkedHashSet<>(1);
// 它的@Bean抽象出来的BeanMethod
private final Set<BeanMethod> beanMethods = new LinkedHashSet<>();
// 导入的XML文件等,一般不用
private final Map<String, Class<? extends BeanDefinitionReader>> importedResources =
new LinkedHashMap<>();
// 导入的注册器,一般我们也不用
private final Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> importBeanDefinitionRegistrars =
new LinkedHashMap<>();
// 跳过的bean方法,我们也不用
final Set<String> skippedBeanMethods = new HashSet<>();
}
我们把所有配置类抽象成一个类之后,我们的处理也会方便很多,接下来,我们正式开始研究ConfigurationClassPostProcessor,看看它是如何处理配置类的。
ApplicationContext会调用所有BeanFactoryPostProcessor的postProcessBeanFactory方法,所以让我们首先看下这个方法:
它主要调用了processConfigBeanDefinitions方法,这里比较长,非核心代码我就尽量换成伪代码
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
configCandidates = new ArrayList<>(); //候选配置BeanDefinition
candidateNames = 取得所有BeanDefinition名
for (String beanName : candidateNames) {
从注册器中取出对应beanName的BeanDefinition,并判断是否是配置类且没有被处理过
将这样的BeanDefinition加入到configCandidates
}
根据优先级对configCandidates排序
配置好BeanName生成器,不重要
初始化环境,也不重要
下面的代码就是关键
// Parse each @Configuration class
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
do {
// 对当前的候选项进行解析
parser.parse(candidates);
parser.validate();
// 取得解析结果
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
// 从配置类中导入BeanDefinition并注册
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);
// 清除候选项
candidates.clear();
// 如果有新注册的BeanDefinition,如果有配置类的就循环此过程全部解析并注册
if (registry.getBeanDefinitionCount() > candidateNames.length) {
String[] newCandidateNames = registry.getBeanDefinitionNames();
Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
Set<String> alreadyParsedClasses = new HashSet<>();
for (ConfigurationClass configurationClass : alreadyParsed) {
alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
}
for (String candidateName : newCandidateNames) {
if (!oldCandidateNames.contains(candidateName)) {
BeanDefinition bd = registry.getBeanDefinition(candidateName);
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
!alreadyParsedClasses.contains(bd.getBeanClassName())) {
candidates.add(new BeanDefinitionHolder(bd, candidateName));
}
}
}
candidateNames = newCandidateNames;
}
}
while (!candidates.isEmpty());
一些不是很重要的代码
}
上面有两个重要的方法:
- parser.parse(candidates)
- this.reader.loadBeanDefinitions(configClass)
同时还有一个需要关注的问题,何时有哪些组件被注册了,循环最后的那个if分支会用到。
parse()
parse有不少重载方法,但是都在调用processConfigurationClass方法,这一块可以说是比较难的点了。
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
return;
}
首先,直接来了我们要关注的内容 shouldSkip():
阅读下源码,就可以知道,它在读取@Conditional注解,并产生Condition对象,根据Condition.matches方法判断是否要跳过,SpringBoot就可以依靠@Conditional来实现自动装配了,不过实际上,就之前看的,我们就知道哪些不需要的配置类根本活不到这里。。。已经被Selector的过滤器过滤掉了。
之后的重点就是
// Recursively process the configuration class and its superclass hierarchy.
SourceClass sourceClass = asSourceClass(configClass, filter);
do {
sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
}
while (sourceClass != null);
this.configurationClasses.put(configClass, configClass);
这个循环可能让我们看不懂,我们马上就要看doProcessConfigurationClass方法,我可以先告诉你,这个方法调用后是返回配置的超类的,我们一般是没有的,这个循环只是为了递归处理配置类及其父类,且顺序是先子类再父类。
然后我先给你个心理准备,这个do while 语句中有着数不清的递归,真的很复杂。当然,心中有图,自然不怕递归,这终究只是一个有向图罢了。。。
心中无图,那只能我来给大家整一个了。
大家可以看到这里有5个箭头,6个框。
- 箭头:一种配置类引入其他组件的方式,只是方式表示,它可以以该种方式引入多个组件
- 框:一个组件,如果是配置类就有资格引入其他组件,如果只是普通组件,则无出度,如果树中的叶子结点,对应的配置类就是普通结点
除了继承这条路是用do-while循环来完成,其他4个箭头都得用递归来实现。。。直到全部引入完成,这就是这个do-while 循环做的事,但是不要忘了,parse()方法外面还有一个循环,告诉我们是一次扫描不完的,我们要记得这个情况,然后去看看为什么扫不完,即使它理论上做得到。
现在,我们大致猜到了doProcessConfigurationClass要做什么了,再去看代码,想必就会比较好看懂了吧。
它大概分了这几个部分
- 处理内部类
- 处理@PropertySource,不是我们关注的重点
- 处理@ComponentScan
- 处理@Import
- 处理@ImportResource,就是导入xml文件,其实就是一个配置类,我不讨论
- 处理@Bean
- 处理接口的默认方法,这是JDK1.8的新特性,也得处理下
- 处理父类
现在我开始一一讲解
- 处理内部类
- 首先找出所有内部配置类(这里有点坑,其实是只要有Component,或者有Bean方法都算),而且视内部类为外部类的导入类
- 通过一个循环加递归,循环处理所有内部类,用processConfigurationClass(内部配置类)递归处理内部配置类
- 维护一个this.importStack,记录已经被递归的配置类,防止循环导入
- 处理@ComponentScan
- 读取ComponentScan的值并且判断是否需要跳过
- 使用componentScanParser.parse处理扫描包
- 这个方法比较复杂,大致就是先扫描出包路径下所有组件,生成BeanDefinition,并且注册入容器,并且返回这些BeanDefinition
- 从上面返回的BeanDefinition中找出配置类,并且用parse(配置类)来实现递归,这里及时处理了新注册的配置类,未逃逸出parse()
- 处理导入类(重点)
- 跟处理内部类一样,循环加递归,this.importStack防止循环导入
- 循环中有三个分支
- 导入的是ImportSelector
1. 看到它的时候我可激动坏了,它就是 AutoConfigurationImportSelector的父类,也就是说,Spring框架就是在这里实现了对所有自动配置类的引入
2. AutoConfigurationImportSelector是一个DeferredImportSelector,处理起来很复杂,我觉得我可能讲不清,大家有兴趣可以去点一下源码,大致就是调用我们之前将的getAutoConfigurationEntry方法,取得自动配置类,再递归调用processImports,被导入的自动配置类则会作为第三种分支进行处理 - 导入的是ImportBeanDefinitionRegistrar,导入注册器不是我要讨论的
- 导入的是其他类
1. 递归调用processConfigurationClass,也就是说,导入默认导入的是配置类,最好不要乱用@Import注解
- 导入的是ImportSelector
- 整个过程中只有递归处理被导入配置类,但是没有去注册组件,会在parse方法后被统一注册
- 处理@Bean
1. 简单粗暴地抽象成BeanMethod存储入configClass
2. 不做任何的注册和处理,会在后续中parse外统一注册,基本可以猜到parse外的循环就是为了它 - 处理接口
1. 处理所有实现的接口,读取其中默认的@Bean方法
2. 递归接口的父接口 - 处理超类
1. 直接返回父类,用来循环调用doProcessConfigurationClass
现在让我们来看看各种处理过程中处理和注册的情况
方式 | 注册 | 处理 |
---|---|---|
内部类引入 | F | T |
ComponentScan扫描引入 | T | T |
Import导入类 | F | T |
@Bean导入 | F | F |
处理接口 | 不会去注册接口 | T |
处理超类 | 不会去注册超类 | T |
看到了上面这张表,其实我们基本知道我们之后要做什么了。
- 注册导入类(内部类是外部类的导入类)
- 将BeanMethod抽象成BeanDefinition注册入容器
- 通过parse外循环将BeanMethod生成的BeanDefinition中的配置类进行处理
这些操作后,这个表就能把所有F变成T了
其中的this.reader.loadBeanDefinitions其实就是做了上面前两步的事情,源码蛮简单的,不看也罢。
至此基本整个配置完毕,所有组件导入完毕。
自动装配也在处理导入类时通过Selector完成。
后言
这是我第二遍看ConfigurationClassPostProcessor,第一遍连通了SpringBoot的自动装配和Spring容器的启动。真的蛮有收获,也蛮有成就感的。