
《Spring6核心技术》第10章:深度解析@Component注解(注解+案例+时序图+源码)
作者:冰河星球:http://m6z.cn/6aeFbs
博客:https://binghe.gitcode.host
文章汇总:https://binghe.gitcode.host/md/all/all.html
源码地址:https://github.com/binghe001/spring-annotation-book/tree/master/spring-annotation-chapter-10
沉淀,成长,突破,帮助他人,成就自我。
大家好,我是冰河~~
- 本章难度:★★★★☆
- 本章重点:进一步学习并掌握@Component注解向IOC容器中注入Bean的案例和流程,从源码级别彻底掌握@Component注解在Spring底层的执行流程。
本节目录如下所示:
- 学习指引
- 注解说明
○ 注解源码
○ 使用场景
- 使用案例
- 源码时序图
- 源码解析
- 总结
- 思考
一、学习指引
Spring中的@Component注解,你真的彻底了解过吗?
@Component注解可以说是Spring中使用的比较频繁的一个注解了。在项目开发过程中,我们自己编写的类如果想注入到Spring中,由Spring来管理Bean的生命周期,就可以使用@Component注解将其注入到IOC容器中。
并且@Component注解还有三个衍生注解,那就是@Repository、@Service和@Controller注解,并且衍生出的注解通常会在使用MVC架构开发项目时,标注到MVC架构的分层类上。
比如:@Repository通常会被标注到表示dao层的类上,@Service注解通常会被标注到表示Service层的类上,而@Controller注解通常会被标注到表示Controller层的类上。
二、注解说明
关于@Component注解的一点点说明~~
使用Spring开发项目时,如果类上标注了@Component注解,当启动IOC容器时,Spring扫描到标注了@Component注解的单例Bean,就会创建对应的Bean对象并注入到IOC容器中。
2.1 注解源码
IOC容器在启动时,如果扫描到被标注了@Component注解的类,则会将这些类的类定义信息自动注入IOC容器,并创建这些类的对象。
@Component注解的源码详见:org.springframework.stereotype.Component。
从源码可以看出,@Component注解是从Spring2.5版本开始提供的注解,并且@Component注解只能标注到类上。其中只含有一个String类型的value属性,具体含义如下所示。
- value:用于指定注入容器时Bean的id。如果没有指定Bean的id,默认值为当前类的名称。
@Component注解提供了三个衍生注解:分别是:@Repository、@Service和@Controller注解。
(1)@Repository注解
@Repository注解的源码详见:org.springframework.stereotype.Repository。
(2)@Service注解
@Service注解的源码详见:org.springframework.stereotype.Service。
(3)@Controller注解
@Controller注解注解的源码详见:org.springframework.stereotype.Controller。
可以看到,@Repository、@Service和@Controller注解本质上还是@Component注解,这里不再赘述。
2.2 使用场景
在Spring开发项目的过程中,如果需要将自己创建的类注入到IOC容器中,就可以使用@Component注解,也可以使用@Repository、@Service和@Controller注解。
其中,@Component注解一般会被标注到非三层(非MVC架构)类上,而@Repository、@Service和@Controller注解通常会被标注到三层架构的类上。并且@Repository通常会被标注到表示dao层的类上,@Service注解通常会被标注到表示Service层的类上,而@Controller注解通常会被标注到表示Controller层的类上。
这里,需要注意的是,基于Spring的注解开发项目时,必须先将类对象交给Spring管理,然后Spring会处理类中的属性和方法。如果类没有被Spring接管,那么类里面的属性和方法上的注解都不会被解析。
三、使用案例
@Component的实现案例,我们一起实现吧~~
本节,就基于@Component注解、@Repository、@Service和@Controller注解实现简单的案例程序,观察被上述四个注解标注的类是否注入到IOC容器中。具体实现步骤如下所示。
(1)新建ComponentBean类
ComponentBean类的源码详见:spring-annotation-chapter-10工程下的io.binghe.spring.annotation.chapter10.component.ComponentBean。
可以看到,ComponentBean就是一个标注了@Component注解的普通类。
(2)新建RepositoryBean类
RepositoryBean类的源码详见:spring-annotation-chapter-10工程下的io.binghe.spring.annotation.chapter10.component.RepositoryBean。
可以看到,RepositoryBean类就是一个标注了@Repository注解的普通类。
(3)新建ServiceBean类
ServiceBean类的源码详见:spring-annotation-chapter-10工程下的io.binghe.spring.annotation.chapter10.component.ServiceBean。
可以看到,ServiceBean类就是一个标注了@Service注解的普通类。
(4)新建ControllerBean类
ControllerBean类的源码详见:spring-annotation-chapter-10工程下的io.binghe.spring.annotation.chapter10.component.ControllerBean。
可以看到,ControllerBean类就是一个标注了@Controller注解的普通类。
(5)新建ComponentConfig类
ComponentConfig类的源码详见:spring-annotation-chapter-10工程下的io.binghe.spring.annotation.chapter10.config.ComponentConfig。
可以看到,ComponentConfig类上标注了@Configuration,说明ComponentConfig类是一个Spring的配置类,并且使用@ComponentScan注解指定了扫描的包名是io.binghe.spring.annotation.chapter10。
(6)新建ComponentTest类
ComponentTest类的源码详见:spring-annotation-chapter-10工程下的io.binghe.spring.annotation.chapter10.ComponentTest。
可以看到,在ComponentTest类的main()方法中打印了IOC容器中注入的Bean对象的名称。
(7)运行ComponentTest类
运行ComponentTest类的main()方法,输出的结果信息如下所示。
从输出的结果信息可以看出,打印出了被@Component、@Repository、@Service和@Controller注解标注的Bean的名称。
说明:使用Spring开发项目时,如果Spring扫描到类上标注了@Component、@Repository、@Service和@Controller注解的单例Bean,就会创建对应的Bean对象并注入到IOC容器中。
四、源码时序图
结合时序图理解源码会事半功倍,你觉得呢?
本节,就以源码时序图的方式,直观的感受下@Component注解在Spring源码层面的执行流程。@Component注解在Spring源码层面执行的时序图如图10-1~10~3所示。
注意:@Repository、@Service和@Controller注解本质上还是@Component注解,这里就不再单独分析@Repository、@Service和@Controller注解的执行流程。
由图10-1~10-3可以看出,@Component注解在注册Bean的流程中涉及到ComponentTest类、AnnotationConfigApplicationContext类、AbstractApplicationContext类、PostProcessorRegistrationDelegate类、ConfigurationClassPostProcessor类、ConfigurationClassParser类、SourceClass类、ComponentScanAnnotationParser类、ClassPathBeanDefinitionScanner类、ClassPathScanningCandidateComponentProvider类、AnnotationConfigUtils类、BeanDefinitionReaderUtils类、和DefaultListableBeanFactory类。具体的源码执行细节参见源码解析部分。
五、源码解析
源码时序图整清楚了,那就整源码解析呗!
本节,主要分析@Component注解在Spring源码层面的执行流程,结合源码执行的时序图,会理解的更加深刻。
注意:本节的源码分析流程与第9章5.2小节的源码分析流程大体相同,只是多了一个更加细节的分析,这里,只对这些细节点进行详细的分析。所以,本节的源码分析可以结合第9章5.2小节的源码分析共同理解。
(1)解析ConfigurationClassParser类的doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicatefilter)方法
源码详见:org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicatefilter)。
可以看到,在ConfigurationClassParser类的doProcessConfigurationClass()方法中,判断如果本质上是@Component注解(@Repository、@Service和@Controller注解),会调用processMemberClasses()方法处理内部类。
(2)解析ConfigurationClassParser类的processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass,Predicatefilter)方法
源码详见:org.springframework.context.annotation.ConfigurationClassParser#processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass,Predicatefilter)。
可以看到,在processMemberClasses()方法中,会调用sourceClass的getMemberClasses()方法获取SourceClass的集合。
(3)解析SourceClass类的getMemberClasses()方法
源码详见:org.springframework.context.annotation.ConfigurationClassParser.SourceClass#getMemberClasses()。
getMemberClasses()方法的主要作用就是处理标注了@Component、@Repository、@Service和@Controller注解的类的内部类,因为内部类也有可能会标注这些注解。在getMemberClasses()方法中,利用反射拿到类的内部类,将内部类封装成SourceClass,存放到members集合中并返回。
(4)返回ConfigurationClassParser类的processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass,Predicatefilter)方法
此时重点关注如下代码。
可以看到,在ConfigurationClassParser类的processMemberClasses()方法中,如果获取到的内部类集合memberClasses不为空,则遍历获取到的memberClasses集合,使用ConfigurationClassUtils类的isConfigurationCandidate()方法判断内部类上是否有需要处理的注解,如果有需要处理的注解,则将类添加到candidates集合中。
(5)解析ConfigurationClassUtils类的isConfigurationCandidate(AnnotationMetadata metadata)方法
源码详见:org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate(AnnotationMetadata metadata)。
isConfigurationCandidate()方法的作用主要是判断内部类上面是否有需要处理的注解,具体的判断逻辑是:如果是接口,则直接返回false,如果是@Component(含@Repository、@Service和@Controller)、@ComponentScan、@Import、@ImportResource等注解,则返回true。最后判断方法上是否标注了@Bean注解,如果标注了@Bean注解,则返回true。否则,返回false。
(6)返回ConfigurationClassParser类的processMemberClasses(ConfigurationClass configClass, SourceClass sourceClass,Predicatefilter)方法
此时重点关注如下代码。
在processMemberClasses()方法中,首先对获取到的内部类进行排序,随后遍历内部类集合,调用candidate的asConfigClass()方法将内部类封装成ConfigurationClass对象。并传入processConfigurationClass()方法中解析内部类的注解信息。
(7)返回ConfigurationClassParser类的doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicatefilter)方法。
继续分析如下代码片段。
其他分析流程省略,直接来到ClassPathBeanDefinitionScanner类的doScan(String... basePackages)方法。
(8)解析ClassPathBeanDefinitionScanner类的doScan(String... basePackages)方法
源码详见:org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan(String... basePackages)。
可以看到,在ClassPathBeanDefinitionScanner类的doScan()中,会遍历传入的扫描包路径数组,调用findCandidateComponents()方法加载符合一定条件的BeanDefinition。
(9)解析ClassPathScanningCandidateComponentProvider类的findCandidateComponents(String basePackage)方法
源码详见:org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents(String basePackage)
在findCandidateComponents()方法中,会调用scanCandidateComponents()方法来扫描basePackage包下标注了注解的类。
(10)解析ClassPathScanningCandidateComponentProvider类的scanCandidateComponents(String basePackage)方法
源码详见:org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents(String basePackage)。
可以看到,在ClassPathScanningCandidateComponentProvider类的scanCandidateComponents()方法中,会加载basePackage包路径下的资源,将其封装成ScannedGenericBeanDefinition类的对象,并传入isCandidateComponent()方法中对类进行过滤。符合条件时,会将当前ScannedGenericBeanDefinition类的对象存入candidates集合中,最终返回candidates集合。
(11)解析ClassPathScanningCandidateComponentProvider类的isCandidateComponent(MetadataReader metadataReader)方法
源码详见:org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent(MetadataReader metadataReader)。
可以看到,在isCandidateComponent()方法中,首先遍历excludeFilters规则列表,如果匹配到excludeFilters规则,则直接返回false。否则,遍历includeFilters规则,如果匹配到includeFilters规则,则调用isConditionMatch()方法来匹配@Conditional注解的规则。
这里,注意的是在IOC容器启动调用AnnotationConfigApplicationContext类的构造方法时,就会对includeFilters规则列表进行初始化。源码详见:org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#registerDefaultFilters()
可以看到,在registerDefaultFilters()方法中,默认会将@Component注解封装成AnnotationTypeFilter对象并存入includeFilters规则列表中。
(12)返回ClassPathBeanDefinitionScanner类的doScan(String... basePackages)方法
源码详见:org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan(String... basePackages)。此时重点关注如下代码片段。
后续解析AnnotationConfigUtils类的processCommonDefinitionAnnotations()方法和解析registerBeanDefinition()方法的流程与第9章5.2小节的源码分析流程一致,这里不再赘述。
至此,@Component注解在Spring源码层面的执行流程分析完毕。
六、总结
@Component注解介绍完了,我们一起总结下吧!
本章,首先介绍了@Component注解的源码和使用场景,随后介绍了@Component注解的使用案例。接下来,详细介绍了@Component在Spring中执行的源码时序图和源码流程。
七、思考
既然学完了,就开始思考几个问题吧?
关于@Component注解,通常会有如下几个经典面试题:
- @Component注解的作用是什么?
- @Component注解有哪些使用场景?
- @Component注解是如何将Bean注入到IOC容器的?
- @Component注解在Spring内部的执行流程?
- 你在平时工作中,会在哪些场景下使用@Component注解?
- 你从@Component注解的设计中得到了哪些启发?
文章转载自公众号:冰河技术
