SpringBoot的自动装配与Spring框架的注册过程 源码分析

SpringBoot的自动装配与Spring框架的注册过程

前言

最近看了雷丰阳老师的SpringBoot的课程,之前也学了下Spring的源码,就打算简单去看下SpringBoot自动装配的实现,这个算是我的学习笔记,同时也给大家分享下自己的学习成果。

关键点

  1. 配置类和自动配置类的定位
  2. SpringBoot和Spring之间是如何连接的
  3. ConfigurationClassPostProcessor的工作流程
    1. ConfigurationClass
    2. 解析ConfigurationClass的过程
    3. 处理配置类和注册组件
    4. 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之间的关系

  1. 首先pom.xml导入了starter,starter的maven项目中导入我们需要的依赖
  2. 我们导入了所有的自动配置类,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的新特性,也得处理下
  • 处理父类

现在我开始一一讲解

  1. 处理内部类
    1. 首先找出所有内部配置类(这里有点坑,其实是只要有Component,或者有Bean方法都算),而且视内部类为外部类的导入类
    2. 通过一个循环加递归,循环处理所有内部类,用processConfigurationClass(内部配置类)递归处理内部配置类
    3. 维护一个this.importStack,记录已经被递归的配置类,防止循环导入
  2. 处理@ComponentScan
    1. 读取ComponentScan的值并且判断是否需要跳过
    2. 使用componentScanParser.parse处理扫描包
      1. 这个方法比较复杂,大致就是先扫描出包路径下所有组件,生成BeanDefinition,并且注册入容器,并且返回这些BeanDefinition
    3. 从上面返回的BeanDefinition中找出配置类,并且用parse(配置类)来实现递归,这里及时处理了新注册的配置类,未逃逸出parse()
  3. 处理导入类(重点)
    1. 跟处理内部类一样,循环加递归,this.importStack防止循环导入
    2. 循环中有三个分支
      1. 导入的是ImportSelector
        1. 看到它的时候我可激动坏了,它就是 AutoConfigurationImportSelector的父类,也就是说,Spring框架就是在这里实现了对所有自动配置类的引入
        2. AutoConfigurationImportSelector是一个DeferredImportSelector,处理起来很复杂,我觉得我可能讲不清,大家有兴趣可以去点一下源码,大致就是调用我们之前将的getAutoConfigurationEntry方法,取得自动配置类,再递归调用processImports,被导入的自动配置类则会作为第三种分支进行处理
      2. 导入的是ImportBeanDefinitionRegistrar,导入注册器不是我要讨论的
      3. 导入的是其他类
        1. 递归调用processConfigurationClass,也就是说,导入默认导入的是配置类,最好不要乱用@Import注解
    3. 整个过程中只有递归处理被导入配置类,但是没有去注册组件,会在parse方法后被统一注册
  4. 处理@Bean
    1. 简单粗暴地抽象成BeanMethod存储入configClass
    2. 不做任何的注册和处理,会在后续中parse外统一注册,基本可以猜到parse外的循环就是为了它
  5. 处理接口
    1. 处理所有实现的接口,读取其中默认的@Bean方法
    2. 递归接口的父接口
  6. 处理超类
    1. 直接返回父类,用来循环调用doProcessConfigurationClass

现在让我们来看看各种处理过程中处理和注册的情况

方式注册处理
内部类引入FT
ComponentScan扫描引入TT
Import导入类FT
@Bean导入FF
处理接口不会去注册接口T
处理超类不会去注册超类T

看到了上面这张表,其实我们基本知道我们之后要做什么了。

  • 注册导入类(内部类是外部类的导入类)
  • 将BeanMethod抽象成BeanDefinition注册入容器
  • 通过parse外循环将BeanMethod生成的BeanDefinition中的配置类进行处理

这些操作后,这个表就能把所有F变成T了
其中的this.reader.loadBeanDefinitions其实就是做了上面前两步的事情,源码蛮简单的,不看也罢。

至此基本整个配置完毕,所有组件导入完毕。
自动装配也在处理导入类时通过Selector完成。

后言

这是我第二遍看ConfigurationClassPostProcessor,第一遍连通了SpringBoot的自动装配和Spring容器的启动。真的蛮有收获,也蛮有成就感的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值