前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >@ComponentScan原理分析

@ComponentScan原理分析

作者头像
叔牙
修改2022-01-05 09:57:11
7650
修改2022-01-05 09:57:11
举报

一、概念

Configures component scanning directives for use with @Configuration classes. Provides support parallel with Spring XML's <context:component-scan> element.

Either basePackageClasses or basePackages (or its alias value) may be specified to define specific packages to scan. If specific packages are not defined, scanning will occur from the package of the class that declares this annotation.

Note that the <context:component-scan> element has an annotation-config attribute; however, this annotation does not. This is because in almost all cases when using @ComponentScan, default annotation config processing (e.g. processing @Autowired and friends) is assumed. Furthermore, when using AnnotationConfigApplicationContext, annotation config processors are always registered, meaning that any attempt to disable them at the @ComponentScan level would be ignored.

这是@ComponentScan的官方介绍,大致意思就是扫描注册bean的一个注解,会扫描对应路径下被@Component标注的类,和xml方式的<context:component-scan>作用相似,常用的方式是basePackages方式。

@ComponentScan会根据配置扫描路径下被@Component或者被其标注了的注解标注的类,比如@Controller、@Service、@Repository和@Configuration等

二、原理

1.注解介绍

代码语言:javascript
复制
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

  @AliasFor("basePackages")
  String[] value() default {};

  @AliasFor("value")
  String[] basePackages() default {};

  Class<?>[] basePackageClasses() default {};

  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

  Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

  ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

  String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

  boolean useDefaultFilters() default true;

  Filter[] includeFilters() default {};

  Filter[] excludeFilters() default {};

  boolean lazyInit() default false;

}
  • basePackages:要扫描的路径,如果为空,解析的时候会解析被@ComponentScan标注类的包路径
  • basePackageClasses:包路径类,与basePackages互斥
  • nameGenerator:bean名称生成器,在解析注册BeanDefinition的时候用到
  • scopeResolver: 类定义上的@Scope注解解析器,如果没有该注解默认单例
  • scopedProxy:扫描到@Component组件是是否生成代理以及生成代理方式
  • resourcePattern:扫描路径时规则
  • useDefaultFilters:默认为true,扫描@Component标注的类以及衍生注解标注的类,如果为false则不扫描,需要自己指定includeFilters
  • includeFilters:自定义包含过滤器,如果@Component扫描不到或者不能满足,则可以使用自定义扫描过滤器
  • excludeFilters:自定义排除过滤器,和includeFilters作用相反,《基于ComponentScan实现接口分环境和分机房注册》基于该特性实现
  • lazyInit:表示扫描注册BeanDefinition后是否延迟初始化,默认false

我们平时用的最多的就是basePackages,已经做一些定制化扫描时会用到includeFilters和excludeFilters。

2.注解解析与路径扫描

@ComponentScan注解的解析依旧在SpringApplication调用AbstractApplicationContext#refresh的时候触发调用ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry,然后通过ConfigurationClassParser#parse解析,委托给doProcessConfigurationClass方法处理:

代码语言:javascript
复制
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
    throws IOException {

  //...省略

  // Process any @ComponentScan annotations
  Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
      sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
  if (!componentScans.isEmpty() &&
      !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
    for (AnnotationAttributes componentScan : componentScans) {
      // The config class is annotated with @ComponentScan -> perform the scan immediately
      Set<BeanDefinitionHolder> scannedBeanDefinitions =
          this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
      // Check the set of scanned definitions for any further config classes and parse recursively if needed
      for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
        BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
        if (bdCand == null) {
          bdCand = holder.getBeanDefinition();
        }
        if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
          parse(bdCand.getBeanClassName(), holder.getBeanName());
        }
      }
    }
  }

  //...省略
  return null;
}

这里会解析ComponentScans和ComponentScan注解,ComponentScans是多个ComponentScan的组合,开发中很少使用,我们重点分析ComponentScan,this.conditionEvaluator.shouldSkip是解析时检查一些依赖信息,这里之所以用了for循环解析,是因为可能会解析出多处ComponentScan标注信息,继续看ComponentScanAnnotationParser#parse方法:

代码语言:javascript
复制
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
  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);
  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")) {
    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));
  }
  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类扫描器,然后解析@ComponentScan注解的属性配到类扫描器中,包括useDefaultFilters、basePackages、includeFilters和excludeFilters等,我们看一下创建ClassPathBeanDefinitionScanner逻辑:

代码语言:javascript
复制
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
    Environment environment, @Nullable ResourceLoader resourceLoader) {

  Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
  this.registry = registry;

  if (useDefaultFilters) {
    registerDefaultFilters();
  }
  setEnvironment(environment);
  setResourceLoader(resourceLoader);
}

前边有说过useDefaultFilters默认为true,然后调用registerDefaultFilters注册默认过滤器:

代码语言:javascript
复制
protected void registerDefaultFilters() {
  this.includeFilters.add(new AnnotationTypeFilter(Component.class));
  ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
  try {
    this.includeFilters.add(new AnnotationTypeFilter(
        ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
    logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
  }
  catch (ClassNotFoundException ex) {
    // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
  }
  try {
    this.includeFilters.add(new AnnotationTypeFilter(
        ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
    logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
  }
  catch (ClassNotFoundException ex) {
    // JSR-330 API not available - simply skip.
  }
}

添加了扫描Component注解的过滤器,同时添加了支持JSR-250和JSR-330的注解扫描过滤器。

然后ComponentScanAnnotationParser会调用ClassPathBeanDefinitionScanner#doScan进行扫描:

代码语言:javascript
复制
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) {
    Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    for (BeanDefinition candidate : candidates) {
      ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
      candidate.setScope(scopeMetadata.getScopeName());
      String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
      if (candidate instanceof AbstractBeanDefinition) {
        postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
      }
      if (candidate instanceof AnnotatedBeanDefinition) {
        AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
      }
      if (checkCandidate(beanName, candidate)) {
        BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
        definitionHolder =
            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
        beanDefinitions.add(definitionHolder);
        registerBeanDefinition(definitionHolder, this.registry);
      }
    }
  }
  return beanDefinitions;
}

该方法最重要的两个点是调用findCandidateComponents和registerBeanDefinition,先看findCandidateComponents方法

(父类ClassPathScanningCandidateComponentProvider中定义):

代码语言:javascript
复制
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
  if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
    return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
  }
  else {
    return scanCandidateComponents(basePackage);
  }
}

if是处理spring5.0中新增的@Indexed注解的逻辑,else处理常规扫描逻辑,@Indexed本篇不做分析,接着看scanCandidateComponents方法:

代码语言:javascript
复制
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
  Set<BeanDefinition> candidates = new LinkedHashSet<>();
  try {
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
        resolveBasePackage(basePackage) + '/' + this.resourcePattern;
    Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
    boolean traceEnabled = logger.isTraceEnabled();
    boolean debugEnabled = logger.isDebugEnabled();
    for (Resource resource : resources) {
      if (resource.isReadable()) {
        try {
          MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
          if (isCandidateComponent(metadataReader)) {
            ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
            sbd.setResource(resource);
            sbd.setSource(resource);
            if (isCandidateComponent(sbd)) {
              candidates.add(sbd);
            }
          }
        }
        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;
}

这里是先扫描路径,把符合条件的类解析成资源Resource,然后转换成ScannedGenericBeanDefinition,然后把符合要求的返回,解析Resource会调用PathMatchingResourcePatternResolver的getResources方法:

代码语言:javascript
复制
@Override
public Resource[] getResources(String locationPattern) throws IOException {
  Assert.notNull(locationPattern, "Location pattern must not be null");
  if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
    // a class path resource (multiple resources for same name possible)
    if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
      // a class path resource pattern
      return findPathMatchingResources(locationPattern);
    }
    else {
      // all class path resources with the given name
      return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
    }
  }
  else {
    // Generally only look for a pattern after a prefix here,
    // and on Tomcat only after the "*/" separator for its "war:" protocol.
    int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
        locationPattern.indexOf(':') + 1);
    if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
      // a file pattern
      return findPathMatchingResources(locationPattern);
    }
    else {
      // a single resource with the given name
      return new Resource[] {getResourceLoader().getResource(locationPattern)};
    }
  }
}

然后创建ScannedGenericBeanDefinition把Resource设置进去,其本质上也是BeanDefinition:

回到ClassPathBeanDefinitionScanner#doScan方法,把解析到的BeanDefinition做一些前置处理后(比如里边有超出本次解析范围的@Autowired引用等),然后把BeanDefinition注册到BeanDefinitionRegistry中备用。

整个注解扫描和注册BeanDefinition的流程大致如下:

3.BeanDefinition实例化

截止到上一步,ConfigurationClassPostProcessor只是解析@ComponentScan注解扫描@Component标注的类解析成BeanDefinition注册到容器中,但是并没有进行任何实例化,对于我们使用@Component标注的类代码中并不能使用,那么我们就简单看一下BeanDefinition的实例化逻辑。

对于SpringBoot应用,AbstractApplicationContext#refresh调用到

DefaultListableBeanFactory#preInstantiateSingletons进行剩余普通单例bean的实例化操作:

代码语言:javascript
复制
public void preInstantiateSingletons() throws BeansException {
  if (logger.isTraceEnabled()) {
    logger.trace("Pre-instantiating singletons in " + this);
  }

  // Iterate over a copy to allow for init methods which in turn register new bean definitions.
  // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
  List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

  // Trigger initialization of all non-lazy singleton beans...
  for (String beanName : beanNames) {
    RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
    if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
      if (isFactoryBean(beanName)) {
        Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
        if (bean instanceof FactoryBean) {
          final FactoryBean<?> factory = (FactoryBean<?>) bean;
          boolean isEagerInit;
          if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
            isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
                    ((SmartFactoryBean<?>) factory)::isEagerInit,
                getAccessControlContext());
          }
          else {
            isEagerInit = (factory instanceof SmartFactoryBean &&
                ((SmartFactoryBean<?>) factory).isEagerInit());
          }
          if (isEagerInit) {
            getBean(beanName);
          }
        }
      }
      else {
        getBean(beanName);
      }
    }
  }
}

最后调用父类AbstractBeanFactory#getBean进行实例化:

代码语言:javascript
复制
@Override
public Object getBean(String name) throws BeansException {
  return doGetBean(name, null, null, false);
}

实例化流程如下:

完成后被@Component、@Controller、@Service和@Repository标注的类就被实例化并注册到spring上下文容器中,我们在代码中就可以调用了。

总结

对于SpringBoot应用一般场景下我们可以不使用@ComponentScan,因为@SpringBootApplication注解中包含了@ComponentScan:

代码语言:javascript
复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
    @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

  @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
  String[] scanBasePackages() default {};
}

但是如果我们直接使用@SpringBootApplication是无法直接修改@ComponentScan里边的属性配置,比如includeFilters和excludeFilters,我们还需要配合@ComponentScan一起使用。熟悉@ComponentScan工作原理后,我们可以在很多特殊场景做一些自定义扫描和注册,在实际开发中能够提高不少开发效率。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、概念
  • 二、原理
    • 1.注解介绍
      • 2.注解解析与路径扫描
        • 3.BeanDefinition实例化
        • 总结
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档