SpringBoot2.x基础篇:带你了解扫描Package自动注册Bean

知识改变命运,撸码使我快乐,2020继续游走在开源界<br/> 点赞再看,养成习惯<br/> 给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案

我们一直在使用SpringBoot来开发应用程序,但是为什么在项目启动时就会自动注册使用注解@Component@Service@RestController...标注的Bean呢?

推荐阅读

默认扫描目录

SpringBoot把入口类所在的Package作为了默认的扫描目录,这也是一个约束,如果我们把需要被注册到IOC的类创建在扫描目录下就可以实现自动注册,否则则不会被注册。

如果你入口类叫做ExampleApplication,它位于org.minbox.chapter目录下,当我们启动应用程序时就会自动扫描org.minbox.chapter同级目录子级目录下全部注解的类,如下所示:

1. src/main/java 2├── org.minbox.chapter 3│ ├── ExampleApplication.java 4│ ├── HelloController.java 5│ ├── HelloExample.java 6│ └── index 7│ │ └── IndexController.java 8├── com.hengboy 9│ ├── TestController.java 10└──

HelloController.javaHelloExample.java与入口类ExampleApplication.java在同一级目录下,所以在项目启动时可以被扫描到

IndexController.java则是位于入口类的下级目录org.minbox.chapter.index内,因为支持下级目录扫描,所以它也可以被扫描到

TestController.java位于com.hengboy目录下,默认无法扫描到

自定义扫描目录

在上面目录结构中位于com.hengboy目录下的TestController.java类,默认情况下是无法被扫描并注册到IOC容器内的,如果想要扫描该目录下的类,下面有两种方法。

方法一:使用@ComponentScan注解

@ComponentScan({"org.minbox.chapter", "com.hengboy"})

方法二:使用scanBasePackages属性

@SpringBootApplication(scanBasePackages = {"org.minbox.chapter", "com.hengboy"})

注意事项:配置自定义扫描目录后,会覆盖掉默认的扫描目录,如果你还需要扫描默认目录,那么你要进行配置扫描目录,在上面自定义配置中,如果仅配置扫描com.hengboy目录,则org.minbox.chapter目录就不会被扫描。

追踪源码

下面我们来看下SpringBoot源码是怎么实现自动化扫描目录下的Bean,并将Bean注册到容器内的过程。

由于注册的流程比较复杂,挑选出具有代表性的流程步骤来进行讲解。

获取BasePackages

org.springframework.context.annotation.ComponentScanAnnotationParser#parse方法内有着获取basePackages的业务逻辑,源码如下所示:

1Set<String> basePackages = new LinkedHashSet<>(); 2// 获取@ComponentScan注解配置的basePackages属性值 3String[] basePackagesArray = componentScan.getStringArray("basePackages"); 4// 将basePackages属性值加入Set集合内 5for (String pkg : basePackagesArray) { 6 String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), 7 ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); 8 Collections.addAll(basePackages, tokenized); 9} 10// 获取@ComponentScan注解的basePackageClasses属性值 11for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) { 12 // 获取basePackageClasses所在的package并加入Set集合内 13 basePackages.add(ClassUtils.getPackageName(clazz)); 14} 15// 如果并没有配置@ComponentScan的basePackages、basePackageClasses属性值 16if (basePackages.isEmpty()) { 17 // 使用Application入口类的package作为basePackage 18 basePackages.add(ClassUtils.getPackageName(declaringClass)); 19}

获取basePackages分为了那么三个步骤,分别是:

  1. 获取@ComponentScan注解basePackages属性值
  2. 获取@ComponentScan注解basePackageClasses属性值
  3. Application入口类所在的package作为默认的basePackages

注意事项:根据源码也就证实了,为什么我们配置了basePackagesbasePackageClasses后会把默认值覆盖掉,这里其实也不算是覆盖,是根本不会去获取Application入口类的package

扫描Packages下的Bean

获取到全部的Packages后,通过org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan方法来扫描每一个Package下使用注册注解(@Component@Service@RestController...)标注的类,源码如下所示:

1protected Set<BeanDefinitionHolder> doScan(String... basePackages) { 2 // 当basePackages为空时抛出IllegalArgumentException异常 3 Assert.notEmpty(basePackages, "At least one base package must be specified"); 4 Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); 5 // 遍历每一个basePackage,扫描package下的全部Bean 6 for (String basePackage : basePackages) { 7 // 获取扫描到的全部Bean 8 Set<BeanDefinition> candidates = findCandidateComponents(basePackage); 9 // 遍历每一个Bean进行处理注册相关事宜 10 for (BeanDefinition candidate : candidates) { 11 // 获取作用域的元数据 12 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); 13 candidate.setScope(scopeMetadata.getScopeName()); 14 // 获取Bean的Name 15 String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); 16 if (candidate instanceof AbstractBeanDefinition) { 17 postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); 18 } 19 // 如果是注解方式注册的Bean 20 if (candidate instanceof AnnotatedBeanDefinition) { 21 // 处理Bean上的注解属性,相应的设置到BeanDefinition(AnnotatedBeanDefinition)类内字段 22 AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); 23 } 24 // 检查是否满足注册的条件 25 if (checkCandidate(beanName, candidate)) { 26 // 声明Bean具备的基本属性 27 BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); 28 // 应用作用域代理模式 29 definitionHolder = 30 AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); 31 // 写入返回的集合 32 beanDefinitions.add(definitionHolder); 33 // 注册Bean 34 registerBeanDefinition(definitionHolder, this.registry); 35 } 36 } 37 } 38 return beanDefinitions; 39}

在上面源码中会扫描每一个basePackage下通过注解定义的Bean,获取Bean注册定义对象后并设置一些基本属性。

注册Bean

扫描到basePackage下的Bean后会直接通过org.springframework.beans.factory.support.BeanDefinitionReaderUtils#registerBeanDefinition方法进行注册,源码如下所示:

1public static void registerBeanDefinition( 2 BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) 3 throws BeanDefinitionStoreException { 4 5 // 注册Bean的唯一名称 6 String beanName = definitionHolder.getBeanName(); 7 // 通过BeanDefinitionRegistry注册器进行注册Bean 8 registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); 9 10 // 如果存在别名,进行注册Bean的别名 11 String[] aliases = definitionHolder.getAliases(); 12 if (aliases != null) { 13 for (String alias : aliases) { 14 registry.registerAlias(beanName, alias); 15 } 16 } 17}

通过org.springframework.beans.factory.support.BeanDefinitionRegistry#registerBeanDefinition注册器内的方法可以直接将Bean注册到IOC容器内,而BeanName则是它生命周期内的唯一名称。

总结

通过本文的讲解我想你应该已经了解了SpringBoot应用程序启动时为什么会自动扫描package并将Bean注册到IOC容器内,虽然项目启动时间很短暂,不过这是一个非常复杂的过程,在学习过程中大家可以使用Debug模式来查看每一个步骤的逻辑处理。

作者个人 博客 使用开源框架 ApiBoot 助你成为Api接口服务架构师

点赞
收藏

评论区

加载中...

相关推荐

SpringBoot2.x基础篇:编写应用程序时常用的ApplicationEvents

!(https://oscimg.oschina.net/oscnet/up98cf371fa7fc0208adc1e184465cd0efc82.png)知识改变命运,撸码使我快乐,2020继续游走在开源界<br/点赞再看,养成习惯<br/给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案(http

SpringBoot2.x基础篇:灵活的使用外部化配置信息

!(https://oscimg.oschina.net/oscnet/up9b884e3dd97b57725c09c1f5aff2607ab8d.png)知识改变命运,撸码使我快乐,2020继续游走在开源界<br/点赞再看,养成习惯<br/给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案(http

SpringBoot2.x基础篇:Linux后台运行Jar以及Jvm参数调优

!(https://oscimg.oschina.net/oscnet/upc5c9362401c5627495b4406279f3ce4b79a.png)知识改变命运,撸码使我快乐,2020继续游走在开源界<br/点赞再看,养成习惯<br/给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案(http

SpringBoot2.x基础篇:将应用程序打包为可执行Jar

!(https://oscimg.oschina.net/oscnet/up991592576a2fa63c72e03fc287acfa0d469.png)知识改变命运,撸码使我快乐,2020继续游走在开源界<br/点赞再看,养成习惯<br/给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案(http

SpringBoot2.x基础篇:编写应用程序时常用的ApplicationEvents

!(https://oscimg.oschina.net/oscnet/up98cf371fa7fc0208adc1e184465cd0efc82.png)知识改变命运,撸码使我快乐,2020继续游走在开源界<br/点赞再看,养成习惯<br/给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案(http

SpringBoot2.x基础篇:Linux后台运行Jar以及Jvm参数调优

!(https://oscimg.oschina.net/oscnet/upc5c9362401c5627495b4406279f3ce4b79a.png)知识改变命运,撸码使我快乐,2020继续游走在开源界<br/点赞再看,养成习惯<br/给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案(http