前言
SpringBoot是一个基于注解开发的框架,与Spring相比,我们不需要编写繁琐的XML文件去定义Bean之间的关系才能交给Spring容器处理。往往我们可以用最简单的方式,只需要在Bean对象所在类上标注一个@Component注解,SpringBoot运行期间便能扫描到这个类并最终加载到容器中。
例如,我们有一个Bean,在SpringBoot和Spring框架中分别需要以下配置。
以SpringBoot的方式配置
@Component
public class TransactionBean {
private String name;
}
以Spring的方式配置
<beans ...>
<bean id="transactionBean" class="com.zst.study.component.TransactionBean">
<property name="age" value="sss"/>
</bean>
</beans>
通过以上两种方式,都可以在程序运行期间为Spring容器注入一个beanName是transactionBean的Bean对象。但是很明显,第一种通过给指定类上标注@Component的方式看起来更“平易近人”、更简单直接。本文将讲解SpringBoot基于注解开发的主要原理。
准备工作
使用SpringBoot,我们需先引入spring-boot-starter依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
概述
运行SpringBoot项目,我们需要定义一个启动类,并且为其标注@SpringBootApplication注解,接着在Main函数中运行SpringApplication.run(xxx.class, args),当run方法结束后,即代表着SpringBoot项目启动完成,我们便可以访问Spring容器中的各种资源,比如一个通过SpringBoot构建的WEB项目,当run方法完成时刻,tomcat服务器随即启动成功,我们便可以通过http请求方式访问接口资源。
//SpringBoot的核心注解,一切将以这里为起点
@SpringBootApplication
public class MainSpringBootController {
public static void main(String[] args){
//run方法执行完成后,SpringBoot项目启动成功
ConfigurableApplicationContext run = SpringApplication.run(MainSpringBootController.class, args);
}
}
是的,一个简易的SpringBoot项目就是这么运行的,所以你应该猜到SpringBoot基于注解开发的起点将是这个@SpringBootApplication注解和SpringApplacation#run(...)方法,因此本文将会以这两个点讲解基于注解开发的主要原理。
@SpringBootApplication
@SpringBootApplication是一个组合注解,它由三个核心注解组成:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。@SpringBootConfiguration本质上是@Configuration注解,告诉Spring,启动类也是一个Bean对象。@ComponentScan是扫描除了启动类这个Bean以外的Bean。@EnableAutoConfiguration主要用于将第三方组件引入项目中。
@SpringBootConfiguration
可以看到,其实@SpringBootConfiguration是由@Configuration组成,也就是说它其实就是一个Configuration配置类。通常按照Spring的一些默认规范,在配置类上会标注此注解,当然也可以标注@Component,因为@Configuration也是由@Component组成。
@ComponentScan
从它的中文译名“组件扫描”,我们大概就能猜到它是干什么的了,没错它正是负责扫描项目中的标注了@Component的类,这些类统称为候选类,因为BeanDefinition对象的创建来源就是经过筛选、符合条件的候选类。
那么启动类上的@ComponentScan又是在何时、何处开始工作的呢?其实@ComponentScan的处理是在SpringApplication#run(...)#refreshContext(...)方法进行的。
在SpringApplication#run(...)方法中,两个方法尤为关键,它们是prepareContext(...)和refreshContext(context)方法,prepareContext(...)#load(...)方法里面会为启动类创建一个BeanDefinition对象并注入到容器中,而refreshContext(context)方法则将以启动类的BeanDefinition对象作为起点,把所有通过注解标注的Bean、或者依然通过Spring的老方法XML形式配置的Bean都逐个找出来为它们执行生命周期相关方法。
@ComponentScan注解处理细节本小节不再过多讲述,将在后面的“SpringApplication#run(...)方法”章节深入解析。
@EnableAutoConfiguration
@EnableAutoConfiguration注解是用来实现自动装配的,什么是自动装配呢?简单的理解就是,我们知道在SpringBoot程序启动过程中会扫描发现自己写的各种Bean并最终加载到容器中去,因为这些Bean上标注了@Component注解。但是对于第三方组件,又需要做些什么才能引入呢?
没错,SpringBoot通过@EnableAutoConfiguration注解引入了一个非常重要的类AutoConfigurationImportSelector.class,这个类将会扫描项目中的所有JAR包下的META-INF/spring.factories文件,只要第三方组件遵循规范,将需要导入的配置类的全限定类名添加到KEY为org.springframework.boot.autoconfigure.EnableAutoConfiguration下面,那么SpringBoot将会读取到这些配置类的全限定类名,这就是自动装配。有了类的最重要的信息——类路径名,那么SpringBoot后面将会根据它们进一步派生为BeanDefinition对象并最终创建为Bean对象。
此注解的讲解篇幅较长,这里不深入介绍。可以到我的另一篇SpringBoot源码学习之SpringBoot自动装配原理文章查阅。
SpringApplication#run(...)方法
SpringApplication#run(...)方法是SpringBoot项目启动的入口,当它执行完成后,容器所需的资源也随之加载完成,例如Bean对象的创建、配置文件内容加载到环境变量等。
在前文讲解@ComponentScan的小节中提到了,对于注解开发来说,此方法中有两个方法尤为关键,一个是prepareContext(...)方法,一个是refreshContext(...)方法。讲述着两个方法之前,我们先看看ConfigurableApplicationContext对象,此对象将承载SpringBoot项目启动期间创建的环境变量、Bean对象等资源,也就是说它是一个集合了资源环境的对象。
ConfigurableApplicationContext
ConfigurableApplicationContext其实是一个接口,它实现了接口BeanFactory,也就是说本质上这个ConfigurableApplicationContext也是一个BeanFactory,可以通过getBean(...)等方法从容器中获取指定的Bean对象。
如果不是WEB项目,在run方法中一般会创建一个AnnotationConfigApplicationContext实例,它实现了ConfigurableApplicationContext接口、继承了GenericApplicationContext。GenericApplicationContext有一个DefaultListableBeanFactory成员变量,而这个DefaultListableBeanFactory非常重要,DefaultListableBeanFactory即实现了BeanFactory接口也实现了BeanDefinitionRegistry接口,当把DefaultListableBeanFactory当作BeanFactory使用的时候,可以通过访问其某些getBean(...)方法从容器获取Bean对象。当把它当作BeanDefinitionRegistry使用的时候,可以调用其registerBeanDefinition(...)方法将BeanDefinition对象注册到容器。
public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
private final AnnotatedBeanDefinitionReader reader;
private final ClassPathBeanDefinitionScanner scanner;
}
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
private final DefaultListableBeanFactory beanFactory;
//当AnnotationConfigApplicationContext创建之际会调用父类的构造函数,这里会直接new一个新的//DefaultListableBeanFactory实例
public GenericApplicationContext() {
this.beanFactory = new DefaultListableBeanFactory();
}
}
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
/** 装载BeanDefinition对象的集合,key是beanName. */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
//将BeanDefinition对象注册进Spring容器...
this.beanDefinitionMap.put(beanName, beanDefinition);
}
@Override
public <T> T getBean(Class<T> requiredType) throws BeansException {
return getBean(requiredType, (Object[]) null);
}
//...其他getBean方法
}
prepareContext(...)方法
在SpringApplication#prepareContext(...)方法中,会做许多事情,例如为ConfigurableApplicationContext对象设置环境变量、对context对象做一些后置处理、注册某些单例的Bean等等、将启动类加载到context对象中。
由于本文主要讲解基于注解开发的主要原理,所以此章节将重点讲解将启动类加载到context对象中的操作,即load(context, sources.toArray(new Object[0]))方法。
public class SpringApplication {
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners...) {
context.setEnvironment(environment);//设置环境变量
postProcessApplicationContext(context);//后置处理
/**SpringApplication.run()方法是先new一个SpringApplication实例对象出
来,此时会添加一些实现了ApplicationContextInitializer接口的应用上下文初始化器
所以当ConfigurableApplicationContext实例对象创建完后,会在这里回调这些初始化器
*/
applyInitializers(context);
// ...注册一些bean
//这个sources就包含了启动类的class对象,所以会在load方法将启动类加载到这个
//context对象中
load(context, sources.toArray(new Object[0]));
}
load(ApplicationContext context, Object[] sources)
在“@SpringBootConfiguration”章节分析了,启动类本质上也是一个Bean对象,那么启动类又是在何时、在何处被SpringBoot识别加载呢?在debug图中,可以知道正是在SpringApplication#run(...)#prepareContext(...)#load(...)方法中完成的。在load(...)中,创建了一个BeanDefinitionLoader的加载对象,将此时ConfigurableApplicationContext对象(实际是AnnotationConfigApplicationContext实例对象)和包含启动类Class对象的数组传入。
接着访问BeanDefinitionLoader#load()方法,从下图看到,最终是通过AnnotatedBeanDefinitionReader#doRegisterBean(...)方法,以启动类的Class对象创建出实例为AnnotatedGenericBeanDefinition的BeanDefinition对象,最后调用DefaultListableBeanFactory#registryBeanDefinition(...)方法将启动类的BeanDefinition对象注入到容器中。
当启动类的BeanDefinition对象注入到容器中以后,SpringBoot基于注解开发的工作已经完成了重要的第一步,那就是找到一个作为扫描起点的地方,即以启动类BeanDefinition对象作为起点,后续会根据此起点将所有候选的Bean对象都找出来 ,无论是自己写的Bean对象还是自动装配导入的配置类Bean对象。这个操作是在后面的refreshContext(...)方法执行的。
refreshContext(...)方法
refreshContext(...)最终会指向ConfigurableApplicationContext#refresh()方法,在这里即是AnnotationConfigApplicationContext#refresh()方法,不过AnnotationConfigApplicationContext并没有重写refresh()方法,而是调用父类之一的AbstractApplicationContext#refresh()方法。
refresh()方法
在AbstractApplicationContext#refresh()方法里面,会执行12个方法,不过本小节并不会逐一讲解这12个方法,但是会讲解跟基于注解开发的主要原理想关联的两个重要方法:invokeBeanFactoryPostProcessors(beanFactory)方法以及finishBeanFactoryInitialization(beanFactory)方法
public class SpringApplication {
private void refreshContext(ConfigurableApplicationContext context) {
....
refresh((ApplicationContext) context);
}
protected void refresh(ConfigurableApplicationContext applicationContext) {
//刷新应用上下午,Spring容器创建后的完善工作大部分都在这里完成
applicationContext.refresh();
}
}
public abstract class AbstractApplicationContext extends DefaultResourceLoader
implements ConfigurableApplicationContext {
@Override
public void refresh() throws BeansException, IllegalStateException {
//准备此上下文以进行刷新.
prepareRefresh();
//告诉子类刷新内部 Bean 工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
//准备 Bean 工厂以用于此上下文
prepareBeanFactory(beanFactory);
try {
//允许在上下文子类中对 Bean 工厂进行后处理.
postProcessBeanFactory(beanFactory);
//调用在上下文中注册为 Bean 的工厂处理器
invokeBeanFactoryPostProcessors(beanFactory);
//注册拦截 Bean 创建的 Bean 处理器。
registerBeanPostProcessors(beanFactory);
// 为此上下文初始化消息源
initMessageSource();
// 初始化此上下文的事件多播程序.
initApplicationEventMulticaster();
//初始化特定上下文子类中的其他特殊 Bean.
onRefresh();
//检查侦听器 Bean 并注册它们
registerListeners();
//实例化所有剩余的(非 lazy-init)单例。
finishBeanFactoryInitialization(beanFactory);
//最后一步:发布对应事件
finishRefresh();
}cathc(Excepton e){
// 异常处理
}
}
}
invokeBeanFactoryPostProcessors(beanFactory) 方法
这个方法其实主要是找到一些实现了特定接口的Bean,回调其重写的抽象方法。比如,会去寻找实现了接口为BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor的Bean,分别回调重写的postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)和postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)方法。
此invokeBeanFactoryPostProcessors(beanFactory) 方法执行以前,SpringBoot会向容器注册一个ConfigurationClassPostProcessor的BeanDefinition对象,它正是实现了BeanDefinitionRegistryPostProcessor接口,在它的重写方法里面正是会以启动类为起点,去扫描其他地方存在的候选类并派生为BeanDefinition对象。
ConfigurationClassPostProcessor
在这个类的注释上,是这么说的:用于引导处理@Configuration。不过它能做的事情远不仅限于处理@Configuration标注的类,它会将所有标注了如@Configuration、@Component、@Import、@ComponentScan等注解的候选类找出来,并经过验证筛选后派生为BeanDefinition对象注入到容器中。
在ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry(registry)方法中,会通过processConfigBeanDefinitions(registry)方法实现解析上述注解的逻辑。通过下面伪代码可以看到,首先做的第一步事情就是将传进来的BeanDefinitionRegistry对象中已经注册的BeanDefinition对象名称都找出来,经过筛选得到启动类以及它的类元数据(包括注解元数据),然后创建一个ConfigurationClassParser对象,再调用其parse(...)方法找到所有的候选类,最后创建一个ConfigurationClassBeanDefinitionReader对象并调用loadBeanDefinitions(...)方法将候选类派生为BeanDefinition对象。
public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
PriorityOrdered, ResourceLoaderAware, ApplicationStartupAware, BeanClassLoaderAware, EnvironmentAware {
//...回调BeanDefinitionRegistryPostProcessor的抽象方法
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
//这个方法便是真正开始寻找所有候选类的起点
processConfigBeanDefinitions(registry);
}
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
//启动类之前被创建为BeanDefinition对象,所以在这里会找到启动类的名字
String[] candidateNames = registry.getBeanDefinitionNames();
//...筛选符合特定条件的配置类,启动类将保留下来
//创建ConfigurationClassParser对象由它以启动类为起点开始解析所有候选类
ConfigurationClassParser parser = new ConfigurationClassParser(
this.metadataReaderFactory, this.problemReporter, this.environment,
this.resourceLoader, this.componentScanBeanNameGenerator, registry);
//启动类在configCandidates集合里面
Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
//开始以启动类为起点解析
parser.parse(candidates);
//...
// 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());
}
//configClasses装载了所有候选类,通过ConfigurationClassBeanDefinitionReader#loadBeanDefinitions方法为它们创建BeanDefinition对象
this.reader.loadBeanDefinitions(configClasses);
}
}
parse(...)方法
在ConfigurationClassParser#parse(...)方法里面便是真正处理注解的地方了,在下图可以看到,在processConfigurationClass(...)#doProcessConfigurationClass(...)方法里面将会处理@Component、@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean等注解。
这里将挑选@Component、@ComponentScan、@Import三个常用的注解进行讲解,剩下的注解由于讲解起来也颇费篇幅,大家有兴趣的可以自行研究。
@Component的处理
@Component的处理是在ConfigurationClassParser#processMemberClasses(...)进行的。如果一个标注了@Component的类,含有内部成员类时,当它们符合两种情况时,会交给ConfigurationClassPostProcessor继续处理。第一种情况是,内部类标注了@Component、@ComponentScan、@Import、@ImportResource任意一个注解的。第二种情况是如果不标注上面四个注解,但是拥有@Bean标注的方法,则也符合条件变成一个候选类。
当符合条件的内部类找到以后,会把这些内部类变成候选类,逐一的递归调用processConfigurationClass(...)方法继续解析。
class ConfigurationClassParser {
private void processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass,Predicate<String> filter) throws IOException {
//获取@Component标注类中的内部成员类
Collection<SourceClass> memberClasses = sourceClass.getMemberClasses();
List<SourceClass> candidates = new ArrayList<>(memberClasses.size());
for (SourceClass memberClass : memberClasses) {
/**
如果内部类是标注了@Component、@ComponentScan、@Import、@ImportSource注解的话,
则是一个标准的候选类;若没标注上面四个注解,但是有@Bean方法,则也是一个候选类,所以
这两种情况都会被扫描出来
*/
if (ConfigurationClassUtils.isConfigurationCandidate(memberClass.getMetadata())){
candidates.add(memberClass);
}
}
//递归调用processConfigurationClass进一步处理
for(SourceClass candidate : candidates){
processConfigurationClass(candidate.asConfigClass(configClass), filter);
}
}
}
@ComponentScan的处理
当启动类经过ConfigurationClassParser#processMemberClasses(...)方法处理@Component注解后,便会对@ComponentScan这个注解进行处理。对于@ComponentScan的处理是委托给ComponentScanAnnotationParser#parser(...)方法的。
那么这个ComponentScanAnnotationParser#parser(...)为我们做了什么事情呢?从下面伪代码看出,@ComponentScan注解是提供给ClassPathBeanDefinitionScanner这个扫描器使用的,而看它的返回值可以猜到,是通过扫描器的scan(...)方法将@Component标注的类扫描出来并封装为BeanDefinitionHolder对象。
class ComponentScanAnnotationParser {
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final
String declaringClass) {
//创建一个扫描标注了@Component注解的类的扫描器
ClassPathBeanDefinitionScanner scanner = new
ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"),
this.environment,this.resourceLoader);
//设置需要保留、引入的类
for (AnnotationAttributes filter :
componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addIncludeFilter(typeFilter);
}
}
//设置需要排除的类
for (AnnotationAttributes filter :
componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
scanner.addExcludeFilter(typeFilter);
}
}
Set<String> basePackages = new LinkedHashSet<>();
String[] basePackagesArray = componentScan.getStringArray("basePackages");
for (String pkg : basePackagesArray) {
String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.
resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
Collections.addAll(basePackages, tokenized);
}
for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
basePackages.add(ClassUtils.getPackageName(clazz));
}
if (basePackages.isEmpty()) {
//当不在@Component指定basePackages和basePackageClasses时,会将启动类
//的路径作为默认扫描路径
basePackages.add(ClassUtils.getPackageName(declaringClass));
}
/**这里我们只需要记住,调用ClassPathBeanDefinitionScanner的doScan方法将
标注了@Component的所有类都找出来并派生为BeanDefinition对象
*/
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
}
在这里我们需要重点关注三个关键属性:includeFilters、excludeFilters、basePackages。includeFilters指的是需要保留、能被扫描出来的,excludeFilters则同includeFilters相反,是排除、不能被扫描出来的,basePackages则是该ClassPathBeanDefinitionScanner扫描器的扫描路径。
当ClassPathBeanDefinitionScanner扫描器在ComponentScanAnnotationParser#parser(...)方法通过new关键字创建出来的时候,会为其includeFilters注册默认的过滤器,其中就包含指定了Component注解的AnnotationTypeFilter过滤器,也就是说扫描器将会扫描出标注了@Component注解的类。
在启动类上的@SpringBootApplication注解里面的@ComponentScan注解默认的excludeFilters过滤器有两个,分别是TypeExcludeFilter和AutoConfigurationExcludeFilter,也就是说当扫描器扫描时会排除这两种类。
basePackages这个属性就是扫描器的作用范围、扫描路径了。但它默认是空的,从伪代码看到,当扫描路径是空的时候,会将当前类的路径作为默认扫描路径。也就说当以启动类为起点时,扫描路径将是启动类当前的路径,和启动类同路径或其路径下的@Component的类就会被扫描出来。
当includeFilters、excludeFilters、basePackages三个重要属性都设置给扫描器后,最终将进到其doScan(String... basePackages)方法,在这里面会做两件事情:找到候选类并派生为BeanDefinition对象、将这些BeanDefinition对象注册到容器中去。
第一件事,先根据basePackages扫描路径,将该路径下的所有class资源找出来,结合excludeFilters和includeFilters过滤器,最终筛选出合格的候选类,然后为它们创建实际类型为ScannedGenericBeanDefinition的BeanDefinition对象。
第二件事情,就是先判断当前容器有没有上一步扫描出来的候选的BeanDefinition对象,如果没有就将它们注入容器。
当启动类第一次被prepareContext(...)#load(...)方法加载为BeanDefinition对象时,再到进入到ConfigurationClassPostProcessor#parse(...)#processConfigurationClass(...)#doProcessConfigurationClass(...)时,以启动类的BeanDefinition对象为起点最终进入处理@ComponentScan注解的扫描器时,启动类所在路径下的所有@Component标注的类都会被找到。也就是说,SpringBoot基于注解开发的工作已经完成了最重要的一步:寻找、发现。
@Import的处理
处理@Import注解是在ConfigurationClassParser#processImports(...)方法,简单来说@Import注解的功能就是导入一个Class对象,这个Class对象将会作为一个Bean候选类对象被处理,就像@Componet标注的类一样。在processImports(...)方法里,根据导入的Class对象的不同有不同的处理,一共有三种情况。
①Import进来的class对象实现了ImportSelector接口
对于实现了ImportSelector接口的类对象来说,还会根据其是否实现了DeferredImportSelector接口(中文是延迟导入选择)变成两种处理方式,如果是的话,则通过ConfigurationClassParser的一个类型是DeferredImportSelectorHandler的对象的handle(...)方法去处理。如果不是延迟导入,则将@Import注解中的value数组中的Class<?>对象都找出来,接着递归调用ConfigurationClassParser#processImports(...)方法。
而@EnableAutoConfiguration注解的中导入AutoConfigurationImportSelector类对象便是即实现了ImportSelector接口又实现了DeferredImportSelector接口。
针对实现了DeferredImportSelector接口导入类的处理,篇幅也比较长,大家可以看这篇文章SpringBoot源码学习之SpringBoot自动装配原理。
②Import进来的class对象实现了ImportBeanDefinitionRegistrar接口
实现了ImportBeanDefinitionRegistrar接口的类,将会通过反射的方式生成一个Bean对象,并将环境变量environment、bean的注册工厂registry等属性值赋值给该Bean对象。接着会把这个Bean对象放到载入类的importBeanDefinitionRegistrars集合中去。这种ImportBeanDefinitionRegistrar类型的Bean对象并将会在后面一定时机回调其重写的registerBeanDefinitions(...)方法。
而@EnableAutoConfiguration注解的中导入的AutoConfigurationPackages.Registrar类,就是实现了ImportBeanDefinitionRegistrar接口,所以后面回调其registerBeanDefinitions(...)时将会创建一个beanName是AutoConfigurationPackages、类型是BasePackagesBeanDefinition的BeanDefinition对象注入容器,并且会将载入类的所在路径赋值给BasePackagesBeanDefinition。也就是说当处理启动上的这个AutoConfigurationPackages.Registrar类时,会创建出一个包含了和启动类同路径的BeanDefinition对象并注入容器,那么这个拥有启动类所在路径的BeanDefinition对象又会有什么作用呢?
其实这个也跟SpringBoot的自动装配有关系,例如当我们引入Mybatis框架时,Mybatis肯定也需要知道哪里有@Mapper注解标注的类,因此需要一个扫描器将它们扫描出来,而不手动指定扫描路径时,便会用到这个@EnableAutoConfiguration注解最终向容器注入的beanName是AutoConfigurationPackages、类型是BasePackagesBeanDefinition的Bean对象,扫描路径便是默认的启动类所在路径。
③Import进来的class对象既没有实现ImportSelector也没有实现ImportBeanDefinitionRegistrar接口
那么这种情况,将会把导入的class对象作为普通的Configuration配置类,递归调用ConfigurationClassParser#processConfigurationClass(...)方法处理,即像启动类对象一样解析@ Component的内部类、Property注解、@ComponentScan、@Import等注解。
最后,当doProcessConfigurationClass(...)方法完成以后,会将候选类的信息封装为ConfigurationClass对象并存到ConfigurationClassParser的configurationClasses集合中。后面将根据此集合内容去创建BeanDefintion对象。
本小节总结:
基于注解开发的主要原理其实可以分为两部分:发现(候选类 ——>BeanDefinition对象)和注册(BeanDefiniton——>Bean)。而讲解完三个常用注解@Component、@ComponentScan、@Import后,相信大家已经有一个清晰的认识了,就是截止现在,SpringBoot通过ConfigurationClassParser#parse(...)方法完成了发现候选类这部分的重要工作,剩下的就是将候选类变成BeanDefinition对象、Bean对象。
loadBeanDefinitions(...)方法
在上一章节讲述了SpringBoot通过ConfigurationClassParser#parse(...)方法发现候选类后,那么剩下的工作是要将它们派生为BeanDefinition对象了。这部分工作则是通过ConfigurationClassBeanDefinitionReader#loadBeanDefinitions(...)完成的。
可以看到,在loadBeanDefinitions(...)方法又是通过loadBeanDefinitionsForConfigurationClass(...)去真正处理将候选类创建为BeanDefinition对象的。
class ConfigurationClassBeanDefinitionReader {
public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
TrackedConditionEvaluator trackedConditionEvaluator = new
TrackedConditionEvaluator();
for (ConfigurationClass configClass : configurationModel) {
loadBeanDefinitionsForConfigurationClass(configClass,
trackedConditionEvaluator);
}
}
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator
trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) &&
this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
} this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
//将候选类自身注册为BeanDefinition对象
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
//将候选类的@Bean方法注册为BeanDefiniton对象,@Bean的处理有些特别
//其BeanDefinition对象的BeanClass是宿主类
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
//处理@ImportResource导入的配置了Bean的xml文件
loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
/**处理@Import注入的实现了ImportBeanDefinitionRegistrar接口的候选类,因为在
ConfigurationClassParser#parse(...)#processImports(...)将此类型的候选类提前创建为
BeanDefinition对象了,所以在这里是回调其registerBeanDefinitions(...)方法*/
loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}
}
首先是针对候选类自身,通过registerBeanDefinitionForImportedConfigurationClass(configClass)方法创建为BeanDefinition对象,对于候选类自身的处理并不复杂,只是将候选类的注解元数据对象(AnnotationMetadata)作为AnnotatedGenericBeanDefinition的构造函数创建一个实际类型为AnnotatedGenericBeanDefinition的BeanDefinition对象。它包含候选类的class信息。
接着是处理候选类的标注了@Bean注解的方法,对于@Bean的BeanDefinition,其class是原始类的class,因为@Bean方式创建的Bean对象正是通过原始类的Bean对象调用@Bean方法创建处理的,具体逻辑大家有兴趣可以自行研究,针对@Bean的处理,这里只讲结论不讲过程。
接着便是处理候选类中@ImportResource导入的资源文件,@ImportResource注解在“parse(...)”方法章节没有讲述,但是我们只要知道,对于资源文件的处理方式和Spring读取XML文件的处理方式大同小异,所以有兴趣的话大家可以自行研究。
最后,便是处理候选类中的importBeanDefinitionRegistrars集合的内容,这个集合中的元素都是实现了ImportBeanDefinitionRegistrar接口的对象,并且这些对象早在parse(...)processImports(...)通过反射提前创建出来了,在loadBeanDefinitionsFromRegistrars(...)方法里面其实就是回调其重写的registerBeanDefinitions(...)方法。而不同的实例对象,其实现逻辑也不一样,想讲也无从下手。
但是前面讲过,@EnableAutoConfiguration注解中的@AutoConfigurationPackage注解最终会导入AutoConfigurationPackages.Registrar.class,而这个Registrar的重写方法其实就是往容器注入了一个名称为org.springframework.boot.autoconfigure.AutoConfigurationPackages的BeanDefinition对象,而它拥有的重要属性就是启动类的所在路径,因为这个AutoConfigurationPackages是给第三方组件当作默认扫描路径使用的,例如Mybatis,当我们不手动指定扫描路径,则会利用AutoConfigurationPackages找到默认路径去扫描@Mapper标注的类。
总结
当前面两个小节的内容讲解完后,ConfigurationClassPostProcessor#parse(...)方法和loadBeanDefinitions(...)方法其实就是做了基于注解开发的最重要的部分:将候选类找出来并派生为BeanDefinition对象!
finishBeanFactoryInitialization(beanFactory)方法
在invokeBeanFactoryPostProcessors(beanFactory)方法章节中,我们知道了通过@Component、@ComponentScan、@Import、@ImportResource等注解标注的类或资源最终会通过ConfigurationClassPostProcessor#parse(...)方法和loadBeanDefinitions(...)方法将候选类都找出来并最终派生为BeanDefinition对象,那么在finishBeanFactoryInitialization(beanFactory)方法里面,就会将所有找出来的候选BeanDefinition对象创建为Bean对象。
将BeanDefinition对象变为Bean对象的过程最终是通过DefaultListableBeanFactory#preInstantiateSingletons()方法处理的,最终会通过AbstractBeanFactory#getBean(String name)方法去执行一个Bean的生命周期涉及的方法创建Bean对象。其实这部分也是直接调用Spring创建Bean的流程,跟基于注解开发的已无太多关联,因此大家有兴趣可以自行研究,本文不再研究。
要说唯一有关系的便是在AbstractAutowireCapableBeanFactory#doCreateBean(...)#populateBean(...)方法里面,会对一个半成品(只实例化了一个属性均为null的对象)的Bean对象做初始化操作(初始化就是属性填充),而这里面会用到一个AutowiredAnnotationBeanPostProcessor处理器,它会识别当前需要初始化的Bean是否存在@Autowired标注的对象属性以及@Value标注的值属性,如果存在则通过AutowiredAnnotationBeanPostProcessor将会将需要注入的对象或者配置值找出来并赋值给当前半成品的Bean对象。但是由于本文篇幅有限,对于AutowiredAnnotationBeanPostProcessor的讲解将会在另一篇文章解读。
全文总结
通过前面小节的讲解,我们终于知道SpringBoot基于注解开发的主要原理了,首先是依赖于启动类的@SpringBootApplication注解,该注解的子注解@EnableAutoConfiguration和@ComponentScan尤为重要,它们为扫描、寻找候选类的提供了客观基础。区别是@EnableAutoConfiguration主要用于扫描第三方JAR包下的META-INF/spring.factories下需要自动配置的配置类全限定类名,而@ComponentScan主要是扫描非第三方、自己在项目编写的有@Component等注解的Bean。
当SpringApplication#run(...)方法执行时,首先在prepareContext(...)方法将启动类提前创建为BeanDefinition对象,接着进入refreshContext(...)方法,根据@EnableAutoConfiguration和@ComponentScan两个注解的信息,去找到所有候选类并派生为BeanDefinition对象并最终利用Spring框架原始的创建Bean对象的那套逻辑,去将这些BeanDefinition对象创建为最终的Bean对象。