Spring扫描,扫尽一切困惑

2023年 9月 28日 21.3k 0

扫描的入口

在Spring中,我们只有使用了@ComponentScan注解,Spring才会进行扫描,并且会解析出@ComponentScan注解中指定的包路径作为扫描路径。

@ComponentScan注解并不是写在任何一个类上都会生效,只有写在了Spring的配置类上才会生效,关于Spring的配置类,其实又分为Full和Lite两种配置类,对这块感兴趣的小伙伴,可以在评论区留言“大都督讲得好”,这样能让我有更足的更新动力。

所以Spring扫描的入口是在Spring解析配置类时触发的,Spring解析配置类时会判断当前配置类上是否有@ComponentScan注解,如果有,Spring就会进行扫描。

在源码中,ConfigurationClassPostProcessor会负责解析配置类,ConfigurationClassParser就是配置类解析器,而在ConfigurationClassParserdoProcessConfigurationClass()方法中就会判断并解析配置类上的各个注解,其中就包括了@ComponentScan注解,以下代码就是判断并解析@ComponentScan注解的源码: image.png

扫描的产物

一定要注意,扫描的产物并不是Bean对象,而是BeanDefinition对象,扫描的目的是发现指定路径下定义了哪些Bean,以及这些Bean的定义是什么,Bean定义即BeanDefinition,它代表的就是一个Bean的定义,比如Bean类型、Bean作用域、Bean初始化方法名等等。

扫描的产物一定不是Bean对象,因为比如多例Bean,或者懒加载的单例Bean,它应该是在真正被使用到时才把Bean对象创建出来,而不是在扫描时就把Bean对象给创建出来,扫描只需要找到Bean定义。

扫描的过程

上述源码中,最核心的就是this.componentScanParser.parse()这行代码了,就是这行代码完成了核心扫描逻辑。在parse()方法中,首先会构造一个ClassPathBeanDefinitionScanner扫描器(下文简称扫描器),并且默认情况下会在扫描器中注册一个includeFilters,源码为:

构造扫描器:

image.png

注册includeFilters(请记住这个Filter): image.png

AnnotationBeanNameGenerator

然后会设置扫描器的beanNameGenerator,beanNameGenerator的作用是负责生成所扫描到的Bean的名字,默认实现为AnnotationBeanNameGeneratorAnnotationBeanNameGenerator能够解析@Component注解得到beanName,如果@Component注解没有指定beanName,AnnotationBeanNameGenerator则会利用将类名首字母小写(并不严谨,大家可以自行看下面方法的实现逻辑)作为beanName,在源码中就是下面这行代码会生成默认的beanName:

image.png

设置IncludeFilter和ExcludeFilter

我们在使用@ComponentScan注解时,除开可以指定扫描路径,还可以设置IncludeFilter和ExcludeFilter,所以,接下来Spring就会把@ComponentScan注解中所定义的IncludeFilter和ExcludeFilter设置给扫描器,后续扫描过程中就会利用这两个Filter进行过滤。

比如,在@ComponentScan注解中,我们可以这样来定义IncludeFilter和ExcludeFilter:

image.png

@ComponentScan中分别定义了一个includeFilters和excludeFilters,通过以上代码可以看到includeFilters对应的Filter会过滤掉指定的UserService类型,而excludeFilters对应的Filter的过滤逻辑是自定义的,具体逻辑在DaduduTypeFilter中:

image.png

设置lazyInit

在Spring中,默认情况下Bean都是非懒加载的,而我们可以直接通过@ComponentScan设置lazyInit = true,使得扫描出来的Bean都是懒加载的。所以在设置完IncludeFilter和ExcludeFilter后,Spring会取出@ComponentScan中所定义的lazyInit值设置给扫描器。

设置扫描路径

最后,就是将@ComponentScan中设置的扫描路径取出来设置给扫描器,如果@ComponentScan没有设置扫描路径,则去当前正在解析的配置类所在的包路径,比如:

image.png

像以上代码,Spring的扫描路径为AppConfig的包路径。

扫描

构造并设置完扫描器后,就会开始真正的扫描了。

首先,会利用ResourcePatternResolver得到扫描路径下所有的class文件,然后遍历每个class文件并利用MetadataReader得到每个class文件元数据信息,比如类名、继承的父类、实现的接口、类上的注解。

注意,MetadataReader解析class文件底层用的是ASM,而不是Java反射,因为这里考虑到,如果扫描路径下有成千上百个类,如果用反射来解析类,很有可能需要把这些类都加载到方法区中,而扫描过程是发生应用启动过程中的,那么会导致应用启动过程中把扫描路径下的所有类都加载到了方法区中,这就违背了JVM的延迟加载的原则,也就是一个类在真正用到时才被加载。

得到类的元数据信息后,就开始判断当前类是不是被定义成为了Bean,判断流程为:

  • 先用excludeFilters进行过滤,如果当前类符合过滤条件,则排除该类,表示该类不是Bean
  • 如果没有被排除掉,则再用includeFilters来判断,一个类只有符合includeFilters中的某一个Filter的过滤逻辑就可以了,并不需要当前类符合所有的includeFilters的过滤逻辑,所以默认情况下,Spring在构造扫描器时注册了一个includeFilter,而这个includeFilter就是用来判断一个类上是否有@Component的,只要有@Component注解,那么这个类就是一个Bean
  • 符合includeFilter后,会继续判断类上是否有@Conditional条件注解,如果有会验证条件,验证通过才能继续后续的判断
  • 通过上述判断后就会基于当前类生成对应的BeanDefinition对象,然后会继续判断
  • 判断当前类是不是抽象类、接口,如果是则要过滤掉,因为抽象类和接口是不能实例化出来Bean对象的,只有是普通类才能成为Bean,有个特殊点是,如果抽象类中有方法上用到了@Lookup注解,那么这个抽象类也能成为Bean,关于@Lookup注解,本文就不详细介绍了,想了解的小伙伴可以给我留言
  • 一个类通过了上述所有判断后,那么它基本上就是一个Bean了
  • 然后利用前面设置好的BeanNameGenerator生成beanName
  • 由于前面已经生成出来了BeanDefinition对象,所以接下来Spring会尽一步判断类上的各种注解,包括:@Lazy@DependsOn等注解,并将对应信息设置到BeanDefinition对象中
  • 最后,得到beanName和BeanDefinition对象后,就可以将BeanDefinition对象注册到Spring容器中了。

    总结

    以上,就是Spring扫描的过程,总结一句话:Spring在解析配置类上的@ComponentScan时会进行扫描,会把扫描得到的BeanDefinition对象注册到Spring容器中。

    好了,今天的内容就分享到这,举头望明月,低头看源码,我是爱读源码的大都督,关注我,一起品味源码。

    我的公众号:Hoeller,欢迎大家来关注

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论