知识改变命运,撸码使我快乐,2020继续游走在开源界<br/>
点赞再看,养成习惯<br/>
给我来个Star吧, 点击了解下基于SpringBoot的组件化接口服务落地解决方案
咱们一直在使用SpringBoot
来开发应用程序,可是为何在项目启动时就会自动注册使用注解@Component
、@Service
、@RestController
...标注的Bean
呢?html
SpringBoot
把入口类所在的Package
做为了默认的扫描目录,这也是一个约束,若是咱们把须要被注册到IOC
的类建立在扫描目录下就能够实现自动注册,不然则不会被注册。java
若是你入口类叫作ExampleApplication
,它位于org.minbox.chapter
目录下,当咱们启动应用程序时就会自动扫描org.minbox.chapter
同级目录、子级目录下所有注解的类,以下所示:git
. src/main/java ├── org.minbox.chapter │ ├── ExampleApplication.java │ ├── HelloController.java │ ├── HelloExample.java │ └── index │ │ └── IndexController.java ├── com.hengboy │ ├── TestController.java └──
HelloController.java
、HelloExample.java
与入口类ExampleApplication.java
在同一级目录下,因此在项目启动时能够被扫描到。spring
IndexController.java
则是位于入口类的下级目录org.minbox.chapter.index
内,由于支持下级目录扫描,因此它也能够被扫描到。segmentfault
TestController.java
位于com.hengboy
目录下,默认没法扫描到。api
在上面目录结构中位于com.hengboy
目录下的TestController.java
类,默认状况下是没法被扫描并注册到IOC
容器内的,若是想要扫描该目录下的类,下面有两种方法。架构
方法一:使用@ComponentScan注解app
@ComponentScan({"org.minbox.chapter", "com.hengboy"})
方法二:使用scanBasePackages属性框架
@SpringBootApplication(scanBasePackages = {"org.minbox.chapter", "com.hengboy"})
注意事项:配置自定义扫描目录后, 会覆盖掉默认的扫描目录,若是你还须要扫描默认目录,那么你要进行配置扫描目录,在上面自定义配置中,若是仅配置扫描com.hengboy
目录,则org.minbox.chapter
目录就不会被扫描。
下面咱们来看下SpringBoot
源码是怎么实现自动化扫描目录下的Bean
,并将Bean
注册到容器内的过程。spring-boot
因为注册的流程比较复杂,挑选出具备表明性的流程步骤来进行讲解。
在org.springframework.context.annotation.ComponentScanAnnotationParser#parse
方法内有着获取basePackages
的业务逻辑,源码以下所示:
Set<String> basePackages = new LinkedHashSet<>(); // 获取@ComponentScan注解配置的basePackages属性值 String[] basePackagesArray = componentScan.getStringArray("basePackages"); // 将basePackages属性值加入Set集合内 for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); } // 获取@ComponentScan注解的basePackageClasses属性值 for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) { // 获取basePackageClasses所在的package并加入Set集合内 basePackages.add(ClassUtils.getPackageName(clazz)); } // 若是并无配置@ComponentScan的basePackages、basePackageClasses属性值 if (basePackages.isEmpty()) { // 使用Application入口类的package做为basePackage basePackages.add(ClassUtils.getPackageName(declaringClass)); }
获取basePackages
分为了那么三个步骤,分别是:
@ComponentScan
注解basePackages
属性值@ComponentScan
注解basePackageClasses
属性值Application
入口类所在的package
做为默认的basePackages
注意事项:根据源码也就证明了,为何咱们配置了basePackages
、basePackageClasses
后会把默认值覆盖掉,这里其实也不算是覆盖,是根本不会去获取Application
入口类的package
。
获取到所有的Packages
后,经过org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
方法来扫描每个Package
下使用注册注解(@Component
、@Service
、@RestController
...)标注的类,源码以下所示:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) { // 当basePackages为空时抛出IllegalArgumentException异常 Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); // 遍历每个basePackage,扫描package下的所有Bean for (String basePackage : basePackages) { // 获取扫描到的所有Bean Set<BeanDefinition> candidates = findCandidateComponents(basePackage); // 遍历每个Bean进行处理注册相关事宜 for (BeanDefinition candidate : candidates) { // 获取做用域的元数据 ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); // 获取Bean的Name String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } // 若是是注解方式注册的Bean if (candidate instanceof AnnotatedBeanDefinition) { // 处理Bean上的注解属性,相应的设置到BeanDefinition(AnnotatedBeanDefinition)类内字段 AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } // 检查是否知足注册的条件 if (checkCandidate(beanName, candidate)) { // 声明Bean具有的基本属性 BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); // 应用做用域代理模式 definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); // 写入返回的集合 beanDefinitions.add(definitionHolder); // 注册Bean registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
在上面源码中会扫描每个basePackage
下经过注解定义的Bean
,获取Bean
注册定义对象后并设置一些基本属性。
扫描到basePackage
下的Bean
后会直接经过org.springframework.beans.factory.support.BeanDefinitionReaderUtils#registerBeanDefinition
方法进行注册,源码以下所示:
public static void registerBeanDefinition( BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) throws BeanDefinitionStoreException { // 注册Bean的惟一名称 String beanName = definitionHolder.getBeanName(); // 经过BeanDefinitionRegistry注册器进行注册Bean registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); // 若是存在别名,进行注册Bean的别名 String[] aliases = definitionHolder.getAliases(); if (aliases != null) { for (String alias : aliases) { registry.registerAlias(beanName, alias); } } }
经过org.springframework.beans.factory.support.BeanDefinitionRegistry#registerBeanDefinition
注册器内的方法能够直接将Bean
注册到IOC
容器内,而BeanName
则是它生命周期内的惟一名称。
经过本文的讲解我想你应该已经了解了SpringBoot
应用程序启动时为何会自动扫描package
并将Bean
注册到IOC
容器内,虽然项目启动时间很短暂,不过这是一个很是复杂的过程,在学习过程当中你们能够使用Debug
模式来查看每个步骤的逻辑处理。
做者我的 博客
使用开源框架 ApiBoot 助你成为Api接口服务架构师