likes
comments
collection
share

[SpringBoot源码分析四]:@ComponentScan

作者站长头像
站长
· 阅读数 31

第一步先祝大家中秋国庆快乐

[SpringBoot源码分析四]:@ComponentScan

1. 背景介绍

@ComponentScan是Spring中非常重要的一个类,可以将我们自己写的对象注册到容器中

在默认情况下,@SpringBootApplication中携带的@ComponentScan默认会把启动类路径作为扫描路径,然后看是否携带@Component作为注册的条件之一

public @interface ComponentScan {

   /**
    * 包扫描的路径
    */
   @AliasFor("basePackages")
   String[] value() default {};

   /**
    * 包扫描的路径
    */
   @AliasFor("value")
   String[] basePackages() default {};

   /**
    * 从指定的Class的包路径开始扫描
    */
   Class<?>[] basePackageClasses() default {};

   /**
    * bean名称生产器
    */
   Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

   /**
    * 范围解析器:检查bean是否有@Scope注解注解的解析器
    */
   Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

   /**
    * 如果bean是多例,以什么方式创建
    */
   ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

   /**
    * 是一个关于文件类型的过滤,默认是 * * / *.class
    */
   String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

   /**
    * 是否使用默认的filter扫描类,默认可以扫描@Component@Repository@Service@Controller注解的类。
    */
   boolean useDefaultFilters() default true;

   /**
    * 指定符合扫描条件:为了进一步缩小候选bean的范围
    */
   Filter[] includeFilters() default {};

   /**
    * 指定不符合组件扫描条件
    */
   Filter[] excludeFilters() default {};

   /**
    * 注册的bean是否延迟初始化。
    */
   boolean lazyInit() default false;
}

2. 源码分析

第一步:我们先来到ConfigurationClassParserdoProcessConfigurationClass方法

protected final SourceClass doProcessConfigurationClass(
      ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
      throws IOException {
   ``` java
    //处理标注了@ComponentScans和@ComponentScan的配置类
    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
      sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    //当设置对应的注解和是否需要跳过
    if (!componentScans.isEmpty() &&
          !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
       //遍历所有的componentScan注解:一个类可以使用多个ComponentScan和@ComponentScans
       for (AnnotationAttributes componentScan : componentScans) {
          //通过componentScan扫描器 获取满足条件的BeanDefinitionHolder
          //BeanDefinitionHolder是beanDefinition + beanName
          Set<BeanDefinitionHolder> scannedBeanDefinitions =
                this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
          //检查所有候选BeanDefinitionHolder,看是否是标志了@Configuration注解,并根据需要递归解析
          for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
             //获得原始BeanDefinition
             BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
             //有些可能就是原始的BeanDefinition,并没有进行封装,所有直接getBeanDefinition就是原始的BeanDefinition
             //像RootBeanDefinition会在代理中用到,他的getOriginatingBeanDefinition()就不为空
             if (bdCand == null) {
                bdCand = holder.getBeanDefinition();
             }
             //判断当前BeanDefinition是否标志了@Configuration注解
             if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
                //又递归解析
                parse(bdCand.getBeanClassName(), holder.getBeanName());
             }
          }
       }
    }
   ...
}

当我们Debug的时候,会发现第一次进入此方法时候的sourceClass启动类, 至于为什么是启动类我在前面的章节中已经介绍过了

上面的这块代码很简单,就是判断启动类上是否携带了@ComponentScan, 再然后就根据ConditionEvaluator判断是否需要跳过,最终根据ComponentScanAnnotationParser将符合条件的类转为BeanDefinitionHolder

第二步:做扫描前的准备

紧接着我们看ComponentScanAnnotationParser的parse方法

public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {
   //创建一个类扫描器
   //useDefaultFilters是是否使用默认的filter进行包扫描,也就是会默认添加有关于@Component和@ManagedBean的包含过滤器
   ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
         componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);

   //获取名称生产器
   Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
   //判断用户是否设置过名称生产器的
   boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
   //如果没有设置过就用自己默认的
   //而这个默认的是当时创建ConfigurationClassParser的时候,也会创建ComponentScanAnnotationParser而是在的
   scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
         //设置了就使用空构造器实例化一个
         //正常实例化后应该有对象引用保存,所以我认为这里是看能否创建这个对象
         BeanUtils.instantiateClass(generatorClass));

   //看是否是代理
   ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
   if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
      scanner.setScopedProxyMode(scopedProxyMode);
   }
   else {
      //设置范围解析器
      Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
      scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
   }

   //设置资源文件的类型范围
   scanner.setResourcePattern(componentScan.getString("resourcePattern"));

   //添加包含过滤器:匹配这些过滤器的就可以加入到容器中
   for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
      //includeFilters属性是可以设置多个 typeFilter进行过滤的
      for (TypeFilter typeFilter : typeFiltersFor(filter)) {
         scanner.addIncludeFilter(typeFilter);
      }
   }
   //添加排除过滤器:匹配这些过滤器的就不可以加入到容器中
   for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
      for (TypeFilter typeFilter : typeFiltersFor(filter)) {
         scanner.addExcludeFilter(typeFilter);
      }
   }

   //是否进行懒加载
   boolean lazyInit = componentScan.getBoolean("lazyInit");
   if (lazyInit) {
      scanner.getBeanDefinitionDefaults().setLazyInit(true);
   }

   //获得扫描包路径
   Set<String> basePackages = new LinkedHashSet<>();
   String[] basePackagesArray = componentScan.getStringArray("basePackages");
   for (String pkg : basePackagesArray) {
      //貌似是去除包路径中一些不合法的字符
      String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
            ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
      Collections.addAll(basePackages, tokenized);
   }
   //获得某个类的
   for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
      //从某个类的包路径开始扫描
      basePackages.add(ClassUtils.getPackageName(clazz));
   }
   //如果basePackages和basePackageClasses都没有设置,就用当前类的包路径进行扫描
   if (basePackages.isEmpty()) {
      basePackages.add(ClassUtils.getPackageName(declaringClass));
   }

   //添加一个默认的排除过滤器,貌似是当前类就不用放入容器中了
   scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
      @Override
      protected boolean matchClassName(String className) {
         return declaringClass.equals(className);
      }
   });
   //准备工作做完,开始扫描
   return scanner.doScan(StringUtils.toStringArray(basePackages));
}

当然上面这段代码很长,我这里就总结下:

  • 创建ClassPathBeanDefinitionScanner:为以后的路径扫描做准备
  • 配置对应的包含过滤器和排除过滤器:这里的包含指的是要注册到容器中的意思,排除则反之
  • 确定包扫描的路径:当然如果说没有配置路径,就会依据以下的代配置启动类路径 [SpringBoot源码分析四]:@ComponentScan
  • 最终调用ClassPathBeanDefinitionScanner.doScan(...):开始扫描

第三步:根据包路径扫描注册BeanDefinition

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
   Assert.notEmpty(basePackages, "At least one base package must be specified");
   Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
   //先遍历所有的包路径
   for (String basePackage : basePackages) {
      //查找候选bean的集合
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      for (BeanDefinition candidate : candidates) {
         //通过范围解析器看BeanDefinition是否携带了@Scope注解,如果携带了返回
         ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
         candidate.setScope(scopeMetadata.getScopeName());
         //通过名称生产器获得bean名称
         String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
         //当是成熟的BeanDefinition的时候,进一步处理
         if (candidate instanceof AbstractBeanDefinition) {
            postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
         }
         //当是有注解的BeanDefinition
         if (candidate instanceof AnnotatedBeanDefinition) {
            //获得特定注解上的值,设置到BeanDefinition的属性中
            AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
         }
         //检查bean名称是否冲突了
         if (checkCandidate(beanName, candidate)) {
            //封装成BeanDefinitionHolder:实际上就是保存了BeanDefinition和beanName
            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
            //如果是设置了代理模式,那么就返回一个作用域的BeanDefinitionHolder
            definitionHolder =
                  AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
            //添加到候选beanDefinitions集合中
            beanDefinitions.add(definitionHolder);
            //注册到bean工厂中
            registerBeanDefinition(definitionHolder, this.registry);
         }
      }
   }
   return beanDefinitions;
}

这个方法也很复杂,但是我已经贴心的写好了注释,这里唯一需要注意的就是是如何查找候选bean的呢,那我们紧接着来看findCandidateComponents(...)方法

/**
 *  查找候选的bean
 */
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
   if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
      return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
   }
   else {
      //使用包扫描
      return scanCandidateComponents(basePackage);
   }
}

上面这个方法体现了Spring中两种不同的查找候选Bean的方式

  • ComponentsIndex:componentsIndex不为空说明项目中有spring.component文件,并且includeFilters还支持@Indexed注解,就直接使用spring.component文件中的bean加载到容器中,而不是用包扫描
  • 包扫描

当然我们一般情况下不会去使用ComponentsIndex,componentsIndex自然就会为空,那么就会进行包扫描

那我们就继续看scanCandidateComponents(...)是如何确定候选Bean的

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
   Set<BeanDefinition> candidates = new LinkedHashSet<>();
   try {
      //封装包扫描的全路径,eg:classpath*:org/lzx/springBootDemo/**/*.class
      String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
            resolveBasePackage(basePackage) + '/' + this.resourcePattern;
      //获得该路径下的所有class文件
      Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
      boolean traceEnabled = logger.isTraceEnabled();
      boolean debugEnabled = logger.isDebugEnabled();
      for (Resource resource : resources) {
         ...
         //高版本还会检测文件是否可读
         try {
            //获取元数据读取器:就是有关于这个资源的元数据
            MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
            //判断当前资源(class文件)对应的元数据是否是候选bean
            if (isCandidateComponent(metadataReader)) {
               //将当前资源转为一个已扫描到的BeanDefinition
               ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
               sbd.setSource(resource);
               if (isCandidateComponent(sbd)) {
                  if (debugEnabled) {
                     logger.debug("Identified candidate component class: " + resource);
                  }
                  //将符合条件的加入候选集合中
                  candidates.add(sbd);
               }
               else {
                    ...
               }
            }
            else {
                ...
            }
         }
         catch (FileNotFoundException ex) {
            ...
         }
         catch (Throwable ex) {
            throw new BeanDefinitionStoreException(
                  "Failed to read candidate component class: " + resource, ex);
         }
      }
   }
   catch (IOException ex) {
      throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
   }
   return candidates;
}

上面这个方法看起来很长,其实就是扫描包路径下的所有class文件,然后根据isCandidateComponent(...)方法返回值确定是否是候选Bean

isCandidateComponent(...)就更简单了,其实就是根据我们注册的过滤器进行选择

[SpringBoot源码分析四]:@ComponentScan

我们最后介绍下默认注册的过滤器

  • 排除过滤器
    • 匿名类:是在parse方法中注册的,主要是就是移除当前的声明类,也就是启动类 [SpringBoot源码分析四]:@ComponentScan
    • AutoConfigurationExcludeFilter:如果携带了@EnableAutoConfiguration也就是自动配置类,自动配置类是靠AutoConfigurationImportSelector注册的,所以不需要@ComponentScan去帮忙注册
    • TypeExcludeFilter:本身是一个委托机制的,是获得容器中类型为TypeExcludeFilter的bean,去做排除的
  • 包含过滤器
    • AnnotationTypeFilter:如果类上带有了@Component或者其自注解比如说@Service也视为需要注册到容器中的
      • 这里只接受一个,另外一个只是说判断的注解变了,变成了@ManagedBean

3. 总结

  • ConfigurationClassParser负责找到那些类携带了@ComponentScan
  • 然后确定包扫描路径,包含过滤器,排除过滤器,最终创建扫描类ClassPathBeanDefinitionScanner
  • 扫描类默认扫描包路径下的class文件,然后根据过滤器先进行排除再进行包含的操作,最终便能确定要注册的Bean

最后再次祝大家中秋国庆快乐

[SpringBoot源码分析四]:@ComponentScan