Spring源码学习 - Scanner的工作原理

作者: 疯狂小兵 | 2019-10-29 | 阅读
「编辑」 「本文源码」

Spring 自带的类扫描器为ClassPathBeanDefinitionScanner,在使用注解@ComponentScan,或者xml 配置<context:component-scan/>时都会调用该扫描器扫描指定的目标类。

ClassPathBeanDefinitionScanner最重要的方法是doScan,通过该方法完成 BeanDefinition 的生成和注册, 在扫描时会配置默认的过滤器用于扫描,比如@Component,@ManagedBean,@Named

扫描流程

扫描并注册流程

  1. 解析 basePackages,将其分隔为数组,然后供doScan方法调用
  2. scanCandidateComponents中将其 basePackage 转换为 Class 路径,classpath*:/cn/followtry/**/*.class
  3. 通过默认的资源加载器,也就是类加载器ClassLoader解析指定的路径上的资源。
  4. 判断资源是否可读,即是否存在。存在则创建元数据读入器MetadataReader
  5. 判断是否是候选的组件。先判断是否在排除的过滤器集合中,再判断是否在包含的过滤器集合中,对在包含过滤器中的组件还需要判断是否是匹配上的Condition。可见排除(exclude),的级别比包含(include)高
  6. 对于判断为候选的组件,会生成ScannedGenericBeanDefinition作为转换为BeanDefinition的载体。
  7. 将Resource 信息设置进ScannedGenericBeanDefinition,然后再判断其是否为候选组件,如果是则添加到候选组件列表中。判断条件为该 Bean通过元数据判断是独立的,并且是具体类 或者 抽象类但是有@Lookup注解。参考ClassPathScanningCandidateComponentProvider#isCandidateComponent
  8. 对于AbstractBeanDefinition类型的BeanDefinition设置默认值和Autowire的候选值。参考ClassPathBeanDefinitionScanner#postProcessBeanDefinition
  9. 对于AnnotatedBeanDefinition类型的BeanDefinition,会处理一些公共类型的注解。如@Lazy,@Primary,@DependsOn,@Role,@Description等注解,会将这些注解的 value 数据补充进BeanDefinition。参考方法:AnnotationConfigUtils#processCommonDefinitionAnnotations
  10. 检查候选者信息,如果检查通过则新注册 Bean。判断条件为:1.该 beanName 是否已经注册。2.对于已注册的情况,检查新老 bean 是否兼容(类型相同,source 相同,两个 bean 定义相同)。如果不兼容会抛出异常。参考ClassPathBeanDefinitionScanner#checkCandidate
  11. 构建持有器BeanDefinitionHolder,然后将其注册进 Spring 容器内。参考ClassPathBeanDefinitionScanner#registerBeanDefinition

注意:扫描工具只会将候选的类筛选出来并注册 Bean 信息,但此时的 Bean 还未实例化。只是类的基本信息已经注册进 Spring 容器。

调用入口

sourceClass为参数类,如cn.followtry.boot.java.BriefSpringbootApplication

org.springframework.context.annotation.ComponentScanAnnotationParser

ComponentScanAnnotationParser是给注解@ComponentScan使用的,该类没有标记public修饰符,只能在当前包下使用。入口方法ComponentScanAnnotationParser#parse。刚方法内新实例化了ClassPathBeanDefinitionScanner。并根据注解@ComponentScan的设置值为 scan 设置参数。 如设置

  1. useDefaultFilters : 是否使用默认过滤器
  2. nameGenerator : 指定的命名生成器
  3. scopedProxy: 代理范围
  4. scopeResolver : 代理解析器
  5. resourcePattern : 资源模式
  6. includeFilters:包含的过滤器
  7. excludeFilters:排除的过滤器
  8. lazyInit: 懒加载
  9. basePackages : 基础扫描包路径
  10. basePackageClasses: 指令类,将其所属的包设作为 basePakcage

设置好了这些参数后,就可以调用扫描器的scanner.doScan方法扫描指定的资源了。而入口方法ComponentScanAnnotationParser#parse是在ConfigurationClassParser#doProcessConfigurationClass中调用的。即先判断并解析了@Configuration注解后,才会判断该注解标记的类上是否有@ComponentScan注解

basePackages 和 basePackageClasses 解析的包路径会合并去重。两个可以都设置也可以只设置一个。

org.springframework.context.annotation.ComponentScanBeanDefinitionParser

ComponentScanBeanDefinitionParserComponentScanAnnotationParser允许设置的参数基本相同。需要在方法configureScanner中创建 Scanner实例并为其设置好配置的属性值。

和注解方式的区别是:ComponentScanBeanDefinitionParser不可以设置basePackageClasses

在 Spring 初始化解析 xml 配置标签的时候,方法DefaultBeanDefinitionDocumentReader#parseBeanDefinitions中的delegate.parseCustomElement会调用ComponentScanAnnotationParser.parse来完成<context:component-scan/>的解析。

此处的Parser的查找需要命名空间处理器NamespaceHandler的协助,Spring会通过解析命名空间和META-INF/spring.handlers中配置的关联关系找到ContextNamespaceHandler,然后通过解析xml配置中的解析器名称,就可以找到已经初始化好的解析器。这样就可以调用解析器的解析方法了。


版权声明:本文由 在 2019年10月29日发表。本文采用CC BY-NC-SA 4.0许可协议,非商业转载请注明出处,不得用于商业目的。
文章题目及链接:《Spring源码学习 - Scanner的工作原理》




  相关文章:

「游客及非Github用户留言」:

「Github登录用户留言」:

TOP