本文详细讲解 spring 中加载 BeanDefinition(bean 定义信息)的源码。
首先我们看一张 spring ioc 的大致流程图:
可以看到,加载 BeanDefinition 是后面几个流程的基础!
由于现在定义 bean 都是通过注解方式,基本没有人使用 xml 来定义 bean 了,所以我们只讲通过注解方式加载 bean 定义信息,主要方式有下面三类:
@ComponentScan(+ @Component、@Controller、@Service、@Repository) 这种我们平时开发中最为常用
@Bean(需要配合 @Configuration 使用)
@Import(主要是导入第三方组件 ,springboot 的自动装配用的就是这个)
本文主要讲的就是通过这三种方法加载 BeanDefinition,并将他们放到 beanDefinitionMap 的源码!
入口方法
首先是入口,看过 spring ioc 源码的都知道,ioc 的精髓是 AbstractApplicationContext 的 refresh() 方法。以 springboot 为例的话,启动类中会调用 SpringApplication.run 方法,run 方法中调用 refreshContext 方法,就会进入到 AbstractApplicationContext 的 refresh() 方法。
在 refresh() 方法中,加载 BeanDefinition 是在 invokeBeanFactoryPostProcessors 方法中完成的
寻找扫描 bean 的地方
进入 invokeBeanFactoryPostProcessors 方法,接着往下看,我们的目标是找到扫描 bean 的地方!顺着代码 debug,进到 ConfigurationClassPostProcessor 的 processConfigBeanDefinitions 方法,发现在这个方法里面,首先在系统类中过滤,并找到我们的启动类 xxxApplication 对应的 BeanDefinition,然后创建了一个 ConfigurationClassParser 解析器,这个解析器十分重要,就是通过它来扫描我们加上注解的类,并加载为 BeanDefinition!
进入 parse 方法,继续往下看,一直进入到 ConfigurationClassParser 的 doProcessConfigurationClass 方法,里面我们熟悉的东西了。
通过代码中的注释,我们可以看到,就是在这个方法中,处理 @ComponentScan 的。这个方法才是最终干活的地方!但是这里要注意,这里只处理 @ComponentScan 注解。@Import 和 @ Bean 不是在 parse 方法中处理的!
上面的 componentScans,是 @ComponentScan 注解对应的 AnnotationAttributes 类。启动类上的 @SpringBootApplication 注解中,已经包含了 @ComponentScan 注解,默认扫描的是启动类所在包下的所有组件。
我画了流程图
下面我们就重点看下 doProcessConfigurationClass 方法。
首先 debug 看下效果:
这里获取到的,确实是我们标注了 @Controller,@Component,@Configuration,@Service 这种注解的类对应的 BeanDefinition。
这里的 componentScanParser 是什么呢?
它是前面在创建 Parser 时 new 出来的 ComponentScanAnnotationparser。
接下来进入 componentScanParser.parse 方法看下它是如何解析的。
首先创建了一个 ClassPathBeanDefinitionScanner,一看它的名字,就可以知道它是类路径 bean 定义信息的扫描器,我们要找的就是它!
接着往下看,后面对 scanner 做了很多赋值操作,然后调用了 scanner 的 doScan 方法。
spring 源码中有个规律,凡是以 do 开头的方法,一般就是真正干活的方法!所以我们进入 doScan 方法。
通过注释可以了解到,这个方法扫描了特定的包(默认就是启动类所在的包),并返回已经注册过的 bean definitions。
很显然,里面的 findCandidateComponents(basePackage) 方法就是扫描我们期望的组件的地方
事实也确实如此,所以进入 findCandidateComponents 方法,发现真正干活的是里面的 scanCandidateComponents 方法。
它首先扫描所有的类,构造为 Resource 数组,然后遍历 Resource 数组,筛选出标识了 @Component 等注解的组件。所以接下来的关键就是如何筛选出这些组件。
首先就是根据 Resource,获得一个对应的 SimpleMetadataReader,这个 Reader 里包含了这个 resource 的信息,以及它的注解元数据,此处以 UserController 为例,它的注解是 RestController
然后进入 isCandidateComponent 方法,判断此 Reader 是否和 @Component 注解匹配,由于 @RestController 注解包含了 @Component 注解,所以这边可以匹配通过(有兴趣的可以点进去了解下如何匹配 )。
匹配成功之后,封装成 BeanDefinition(ScannedGenericBeanDefinition ) 并返回,最后得到一个 BeanDefinition 的 set 集合。此时这些 BeanDefinition 只是候选(从名字也可以看出:candidate)的,还没有真正放到容器 beanDefinitionMap(这个 beanDefinitionMap 是最终存放可用的 BeanDefinition 的容器)中。
通过 debug 代码也可以看到,一直到 registerBeanDefinition 方法调用之前,beanDefinitionMap 中还是没有我们自定的组件的!
进入 registerBeanDefinition 方法,一直往下找,找到下图中这个地方
看到了吧,通过 beanName 从 beanDefinitionMap 中查询 BeanDefinition,发现找不到!所以下面就要往这个 map 中添加。
首先加锁,然后往 beanDefinitionMap 加入我们的 beanDefinition。这个时候再看 ioc 容器中的 beanDefinitionMap 时,就有我们的想要的 BeanDefinition 了!
到这个地方为止,@ComponentScan 就真正扫描完成了,BeanDefinition 也加载完成了。
从解析 parser.parse 方法开始,到最后注册 BeanDefinition 为止,我画了张流程图:
结束了吗?还没有。@Bean 和 @Import 注解对应的 BeanDefintion 还没加载。
回到前面调用 parser.parse 所在的方法中,往下面看,reader 的 loadBeanDefinitions 方法,才是处理 @Bean 和 @Import 注解导进来的类的地方,同时这个方法还是 springboot 扫描自动装配类的地方!
回到前面的 doProcessConfigurationClass 方法,也就是处理 @ComponentScan 注解的地方。
这个方法里面,扫描了加了 @Configuration 注解的类,并将它加载为 BeanDefintion。
然后呢,对这些扫描出来的 BeanDefinition,还会做进一步的解析!
通过 debug 可以看出,当我们获得了 BeanDefinition 后,还会以这个类为 ConfigurationClass,递归进行解析。就是在后面的这次解析中,对 @Bean 注解和 @Import 注解的处理做了一些前置准备(只是前置准备,此处并不进行加载)
进入 retrieveBeanMethodMetadata 方法,我们可以看到在里面获取了所有加了 @Bean 注解的方法,然后保存为 MethodMetadata 对象并返回。在外层又将 MethodMetadata 包装为 BeanMethod 对象。到这里暂时就结束了,这个 BeanMethod 对象后续会用到。
然后回到最初调用 parser.parse 的地方,在 pasre 完成后(此时加了 @Configuration、@Controller 等注解的类已经被加载),创建了一个 ConfigurationClassBeanDefinitionReader 读取器,之后调用 reader 的 loadBeanDefinitions 方法进行最终的加载,传入的参数是之前获取的所有的类的 ConfigurationClass。
就是在这个 loadBeanDefinitions 方法中,对 @Bean、@Import 对应的类进行加载的。
进入此方法,发现里面对传入的数据做了循环遍历
继续往下看,看到了前面设置的 BeanMethods ! 很显然,这里就是对 @Bean 进行加载的地方。
由于一个类中可能对多个方法加了 @Bean 参数,所以对 BeanMethod 集合进行遍历调用 loadBeanDefinitionsForBeanMethod 方法,在里面进行 BeanDefinition 的加载。
这个方法很长,但是前面的不重要,都是给 BeanDefinition 设置一些参数,方法的最后,调用了 registry 的 registerBeanDefinition 方法,将这个 BeanDefinition 注册到 beanDefinitionMap 中!这个 registerBeanDefinition 方法前面讲过,这里就不再进去看了。
到这里为止,@Bean 也成功地进行了加载。
我在启动类上添加了一个 Import 注解,导入 User 类,这样启动时就能将 User 纳入 ioc 容器管理
接下来看这个 User 类是如何被加载的。
跟前面加载 @Bean 一样,也是在 loadBeanDefinitionsForConfigurationClass 方法中加载的
我们这个 User 类是 Import 进来的,所以这边 isImported 一定返回的是 true。我们进去看下是如何校验的
可以看到,是通过 importedBy 是否为空来判断的,而这里 importedBy 是我们的启动类,因为我是在启动类上 import 的 User 类。
那么这个 importedBy 是什么时候设置的值呢?
我们依然回到 doProcessConfigurationClass 方法中去寻找
进入 processImports 方法,在方法里面对启动类上的所有 @Import 注解循环做了处理
只看我导入的 User 类,通过 debug 可知,就是在这个 asConfigClass 方法中,将 User 设置为一个 ConfigurationClass
就是在 ConfigurationClass 的构造方法中,把启动类设置到 importedBy 属性上的!
到这里就很明朗了,在 parser.parse 方法中,先将 User 类转化为一个 ConfigurationClass 类,并将由哪个类导入它的(此处是启动类),设置到它的 importedBy 属性中。然后在之后的 loadBeanDefinitions 方法中,判断 ConfigurationClass 的 importedBy 属性是否为空,如果不为空,说明是需要加载的,将它加载为 BeanDefinition,由 ioc 容器管理。
最后加载地方是在判断 configClass.isImported() 为 true 之后进行的。
进入此方法:registerBeanDefinitionForImportedConfigurationClass
可以看到,最后还是通过 registry 的 registerBeanDefinition 方法,加载的 BeanDefinition!
到这里为止,spring 加载 BeanDefinition 的整个流程的源码,就全部介绍完了。