述
接着上文,继续分析配置类的解析的核心方法, 上文中我们留了一个 ConfigurationClassParser.doProcessConfigurationClass()
方法,没有详细分析,下面来看一下这个类具体是怎么处理的
源码跟踪
从上往下依次看
内部类处理
先看一下代码片段
首先判断有没有 @Component
注解, 有的话进入下面的 processMemberClasses()
方法
这里就是对内部类的一个递归处理,我们写个例子看一下
案例
以启动类为例,这里添加两个内部类进去,如下
然后在上面的 processMemberClasses()
方法中打个断点
这里就获取到两个内部类,然后通过 ConfigurationClassUtils.isConfigurationCandidate()
这个方法做个判断,看是否需要处理,需要的话后面再调用 processConfigurationClass()
方法去处理,这两个方法我们上文中详细分析过了,这里就不再赘述了
上面还有一个 importStack
集合,主要是为了防止相互依赖的问题, 比如 A配置类中依赖B,B配置类中又依赖A
@PropertySource 处理
接着往下看,是 @PropertySource
注解的处理, @PropertySource
可以指定一个配置文件,具体加载就是在这里处理的
继续往下
首先获取到文件,然后调用了一个 addPropertySource()
方法,点进去
这里主要是对环境变量的一些处理
@ComponentScan 处理
再往下看是 @ComponentScan
注解的处理,在启动类中的 @ComponentScan
注解如下
这里就配置了个过滤器,用来判断哪些类不需要加载
回到方法中
这里的重点是 this.componentScanParser.parse()
这个方法,他会把 @ComponentScan
注解配置的要扫描的包下的所有的 @Component
修饰的类都扫描出来,进入这个方法
这里主要是获取一些注解中的属性,然后填充到对象中,比较重要的是这个 basePackages
的获取,这里我们可以在注解中手动指定要扫描的包路径,如果没有手动指定的话,会获取当前类所在的包路径,这里就解释了为什么 Spring Boot 项目的启动类为什么要放在最上层的包中
然后继续往下,是调用了一个 doScan()
方法,点进去
这里的重点是 findCandidateComponents()
这个方法, 点进去看一下
然后这里判断是否要处理的方法是 isCandidateComponent()
,如下
这里会调用我们注解中配置的过滤器,判断判断一下是否匹配,然后觉得是否需要处理
案例
这里我们以启动类为例, 看一下他的扫描结果
首先是 scanCandidateComponents()
方法中的返回
然后看一下外层的 findCandidateComponents()
的返回
@Import 处理
@ComponentScan
处理完成之后会处理 @Import
注解, 会调用一个 processImports()
去处理,主要代码如下
这个方法的主要作用是处理 ImportSelector
, DeferredImportSelector
还有 ImportBeanDefinitionRegistrar
这几个类的实现, 我们可以通过实现这几个类,往容器中注入 Bean
案例
关于这几个接口的使用,我们写以下几个类来测试一下
首先,新建 MyImportSelector
实现 ImportSelector
, 代码如下1
2
3
4
5
6
7
8
9
10@Slf4j
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
log.info(this.getClass().getName());
return new String[]{"com.zhou.springboot.example.model.Test"};
}
}
这个类作用就是将返回的数组中对应的类注册到容器中
然后新建 MyDeferredImportSelector
实现 DeferredImportSelector
,代码如下1
2
3
4
5
6
7
8
9
10@Slf4j
public class MyDeferredImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
log.info(this.getClass().getName());
return new String[0];
}
}
这里和上面的 MyImportSelector
的作用其实是一样的
最后一个新建 MyImportBeanDefinitionRegistrar
实现 ImportBeanDefinitionRegistrar
,代码如下1
2
3
4
5
6
7
8
9
10@Slf4j
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {
log.info(this.getClass().getName());
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition();
rootBeanDefinition.setBeanClass(Test.class);
registry.registerBeanDefinition("test2", rootBeanDefinition);
}
}
这里也是往容器中注入一个 BeanDefinition ,最后需要使用 @Import
注解把这几个类手动引入才生效, 以启动类为例,修改如下1
2
3
4
5
6
7@SpringBootApplication
@Import({MyImportSelector.class, MyDeferredImportSelector.class, MyImportBeanDefinitionRegistrar.class})
public class App {
public static void main(String[] args) {
// ... 省略其他代码
}
}
启动项目看一下日志输出
这里三个类的作用其实是一样的, 只是处理的顺序不一样 ImportSelector
> DeferredImportSelector
> ImportBeanDefinitionRegistrar
这种注入方式也不常用,就不详细介绍了
@ImportResource 处理
再往下看,就是对 @ImportResource
这个注解的处理, Spring Boot 中注入 Bean 可以通过注解,也可以通过 xml 的方式, 通过 xml 方式注入的话, 就需要使用 @ImportResource
注解引入具体的 xml ,处理代码如下:
这里,最终只会把文件找出来,然后调用 configClass.addImportedResource()
这个方法添加到一个集合中去, 在这一步并不会做具体的处理
这种方式也基本不常用,也不做过多介绍
@Bean 处理
再往下就是对 @Bean
注解的处理,代码如下
这里会通过 retrieveBeanMethodMetadata()
这个方法找到这个类中,所有的 @Bean
修饰的方法,然后通过 configClass.addBeanMethod()
添加到一个集合中去, 跟 xml 的方式一样,这里只是保存起来不做具体处理
接口默认方法处理
最后是对接口的默认方法处理,如下
点进去如下
这些都处理完成之后,会判断有没有父类,有的话返回去,上层方法会继续循环处理这些东西
最后来看一下 xml 和 @Bean
注入的 bean 是什么时候被处理的
回到最外层的 ConfigurationClassPostProcessor$processConfigBeanDefinitions()
这个方法中, 这个方法就是配置类处理的最外层的方法,我们之前最开始分析的, 进到之前说过的那个 do-while 循环中, 如下
上面的 parse
方法把这些类都处理完毕后,就会调用 loadBeanDefinitions()
这个方法, 在这个方法中,就会处理 xml 和 所有 @Bean
注入的 bean ,如下
总结
本文对上文中的 doProcessConfigurationClass()
方法做了详细的分析, 就是下面这个 8 个模块的处理
可以 debug 跟一下效果会更好