扫描的入口
在Spring中,我们只有使用了@ComponentScan
注解,Spring才会进行扫描,并且会解析出@ComponentScan
注解中指定的包路径作为扫描路径。
而@ComponentScan
注解并不是写在任何一个类上都会生效,只有写在了Spring的配置类上才会生效,关于Spring的配置类,其实又分为Full和Lite两种配置类,对这块感兴趣的小伙伴,可以在评论区留言“大都督讲得好”,这样能让我有更足的更新动力。
所以Spring扫描的入口是在Spring解析配置类时触发的,Spring解析配置类时会判断当前配置类上是否有@ComponentScan
注解,如果有,Spring就会进行扫描。
在源码中,ConfigurationClassPostProcessor
会负责解析配置类,ConfigurationClassParser
就是配置类解析器,而在ConfigurationClassParser
的 doProcessConfigurationClass()
方法中就会判断并解析配置类上的各个注解,其中就包括了@ComponentScan
注解,以下代码就是判断并解析@ComponentScan
注解的源码:
扫描的产物
一定要注意,扫描的产物并不是Bean对象,而是BeanDefinition对象,扫描的目的是发现指定路径下定义了哪些Bean,以及这些Bean的定义是什么,Bean定义即BeanDefinition,它代表的就是一个Bean的定义,比如Bean类型、Bean作用域、Bean初始化方法名等等。
扫描的产物一定不是Bean对象,因为比如多例Bean,或者懒加载的单例Bean,它应该是在真正被使用到时才把Bean对象创建出来,而不是在扫描时就把Bean对象给创建出来,扫描只需要找到Bean定义。
扫描的过程
上述源码中,最核心的就是this.componentScanParser.parse()
这行代码了,就是这行代码完成了核心扫描逻辑。在parse()
方法中,首先会构造一个ClassPathBeanDefinitionScanner
扫描器(下文简称扫描器),并且默认情况下会在扫描器中注册一个includeFilters,源码为:
构造扫描器:
注册includeFilters(请记住这个Filter):
AnnotationBeanNameGenerator
然后会设置扫描器的beanNameGenerator,beanNameGenerator的作用是负责生成所扫描到的Bean的名字,默认实现为AnnotationBeanNameGenerator
,AnnotationBeanNameGenerator
能够解析@Component
注解得到beanName,如果@Component注解没有指定beanName,AnnotationBeanNameGenerator
则会利用将类名首字母小写(并不严谨,大家可以自行看下面方法的实现逻辑)作为beanName,在源码中就是下面这行代码会生成默认的beanName:
设置IncludeFilter和ExcludeFilter
我们在使用@ComponentScan
注解时,除开可以指定扫描路径,还可以设置IncludeFilter和ExcludeFilter,所以,接下来Spring就会把@ComponentScan
注解中所定义的IncludeFilter和ExcludeFilter设置给扫描器,后续扫描过程中就会利用这两个Filter进行过滤。
比如,在@ComponentScan
注解中,我们可以这样来定义IncludeFilter和ExcludeFilter:
@ComponentScan
中分别定义了一个includeFilters和excludeFilters,通过以上代码可以看到includeFilters对应的Filter会过滤掉指定的UserService类型,而excludeFilters对应的Filter的过滤逻辑是自定义的,具体逻辑在DaduduTypeFilter
中:
设置lazyInit
在Spring中,默认情况下Bean都是非懒加载的,而我们可以直接通过@ComponentScan
设置lazyInit = true
,使得扫描出来的Bean都是懒加载的。所以在设置完IncludeFilter和ExcludeFilter后,Spring会取出@ComponentScan
中所定义的lazyInit值设置给扫描器。
设置扫描路径
最后,就是将@ComponentScan
中设置的扫描路径取出来设置给扫描器,如果@ComponentScan
没有设置扫描路径,则去当前正在解析的配置类所在的包路径,比如:
像以上代码,Spring的扫描路径为AppConfig的包路径。
扫描
构造并设置完扫描器后,就会开始真正的扫描了。
首先,会利用ResourcePatternResolver
得到扫描路径下所有的class文件,然后遍历每个class文件并利用MetadataReader
得到每个class文件元数据信息,比如类名、继承的父类、实现的接口、类上的注解。
注意,MetadataReader
解析class文件底层用的是ASM,而不是Java反射,因为这里考虑到,如果扫描路径下有成千上百个类,如果用反射来解析类,很有可能需要把这些类都加载到方法区中,而扫描过程是发生应用启动过程中的,那么会导致应用启动过程中把扫描路径下的所有类都加载到了方法区中,这就违背了JVM的延迟加载的原则,也就是一个类在真正用到时才被加载。
得到类的元数据信息后,就开始判断当前类是不是被定义成为了Bean,判断流程为:
@Component
的,只要有@Component
注解,那么这个类就是一个Bean@Conditional
条件注解,如果有会验证条件,验证通过才能继续后续的判断@Lookup
注解,本文就不详细介绍了,想了解的小伙伴可以给我留言BeanNameGenerator
生成beanName@Lazy
、@DependsOn
等注解,并将对应信息设置到BeanDefinition对象中最后,得到beanName和BeanDefinition对象后,就可以将BeanDefinition对象注册到Spring容器中了。
总结
以上,就是Spring扫描的过程,总结一句话:Spring在解析配置类上的@ComponentScan时会进行扫描,会把扫描得到的BeanDefinition对象注册到Spring容器中。
好了,今天的内容就分享到这,举头望明月,低头看源码,我是爱读源码的大都督,关注我,一起品味源码。
我的公众号:Hoeller,欢迎大家来关注