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

知识改变命运,撸码使我快乐,2020继续游走在开源界

点赞再看,养成习惯

给我来个Star吧,点击了解下基于SpringBoot的组件化接口服务落地解决方案

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

推荐阅读

默认扫描目录

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

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

. src/main/java
├── org.minbox.chapter
│   ├── ExampleApplication.java
│   ├── HelloController.java
│   ├── HelloExample.java
│   └── index
│   │   └── IndexController.java
├── com.hengboy
│   ├── TestController.java
└──

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的业务逻辑,源码如下所示:

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分为了那么三个步骤,分别是:

  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...)标注的类,源码如下所示:

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注册定义对象后并设置一些基本属性。

注册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接口服务架构师

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容