Spring框架解析之@ComponentScan

​本文是Spring IoC容器技术介绍系列文章之一。本文介绍@ComponentScan

1 自动扫描机制

@ComponentScan注解在Spring中用来定义IoC容器需要扫描哪些类文件。在这些类文件中,所有以@Component标注的对象和以@Bean标注的方法都会被自动加载到IoC容器中。

另外,除了@Component注解之外,其他诸如@Controller@Configuration@Service这些继承了@Component注解的注解,也同样适用于上述的扫描规则,均会被自动加载到IoC容器中。

2 IoC容器的启动入口

IoC容器在启动时,需要指定一个或多个配置类。这些配置类会在IoC容器初始化之前,被解析成一个个BeanDefinition对象。正常使用时,一般会在这些配置类上增加@Configuration注解,表明他们是用于容器配置的。但实际上,@Configuration注解是可以省略的。这是因为,这些配置类是通过手动设置的方式,由IoC直接加载进容器,而并未使用到自动扫描机制。

比如说,下面这段代码是完全有效的:

public class Application {
    @Bean NumberStyleFormatter numberStyleFormatter() {
        return new NumberStyleFormatter();
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
        NumberStyleFormatter numberStyleFormatter = applicationContext.getBean(NumberStyleFormatter.class);
        System.out.println(numberStyleFormatter);
    }
}

但即使如此,我们仍建议在这些类上增加@Configuration注解,便于理解。

3 自动扫描机制

默认情况下,只有如下的bean对象会被加载到IoC容器中:

  • 配置类里面使用@Component标注的静态内部类;

  • 配置类里面使用@Bean标注的返回不为void类型的方法;

  • 配置类上通过@Import注解引入的其他类;

当需要IoC容器加载除了上述三种情况之外的其他Bean对象时,则需要使用@ComponentScan注解进行标注。比如下面的代码会让IoC容器扫描并加载org.example.app.bean包下的所有bean对象。

@ComponentScan("org.example.app.bean")
public class Application {
    @Bean NumberStyleFormatter numberStyleFormatter() {
        return new NumberStyleFormatter();
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
        NumberStyleFormatter numberStyleFormatter = applicationContext.getBean(NumberStyleFormatter.class);
        System.out.println(numberStyleFormatter);
    }
}

4 默认扫描路径

如果@ComponentScan注解不带参数,则默认扫描路径是以标注了该注解的类所在包为根路径,其下的所有类和子包下的所有类。

The @SpringBootApplication annotation is often placed on your main class, and it implicitly defines a base “search package” for certain items.

Spring Boot中指出,如果将@SpringBootApplication注解放在启动类上,并让启动类放在工程的最外层的包下面,则会自动扫描所有类文件来加载bean。这个机制的原理是:@SpringBootApplication继承了@ComponentScan注解。所以,@SpringBootApplication在没有指定扫描包路径时,会从当前位置一直往下扫描。

...
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(...)
public @interface SpringBootApplication {
 ...
}

5 源码解释

Spring框架在初始化加载IoC容器时,会先根据配置类的配置信息,找到所有需要加载到IoC容器中@Component类型的类和@Bean类型的方法。它会为这些类和方法创建类型为BeanDefinition的对象,每个类和方法都会创建一个独立的BeanDefinition对象。BeanDefinition中包含了创建这些bean对象时需要使用的相关信息。

为了保证能将符合要求的@Component@Bean对象都找到,Spring IoC容器在启动时,会对每个@Component对象都执行如下操作:

  1. 确认该对象有没有静态内部类,如果有则确认其是否是一个合法的bean,若是则将其也加载到IoC容器中;

  2. 确认该对象有没有使用@Bean标注的方法,如果有则加载到IoC容器中;

  3. 确认该类有没有使用@ComponentScan注解标注,如果有则开始扫描相关路径,将得到的合法的bean对象也加载的IoC容器中

  4. ……

其中的第3步便涉及到对@ComponentScan注解的处理。

 @Nullable
 protected final SourceClass doProcessConfigurationClass(
  ...
  // Process any @ComponentScan annotations
  Set 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 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());
     }
    }
   }
  }
  ...
 }

上面这段源码中,通过this.componentScanParser.parse(...)方法拿到了扫描后的所有bean对象。这部分就是Spring IoC处理@ComponentScan的核心代码。

现在我们还有最后一个问题没有回答。如果@ComponentScan中没有指定扫描路径,程序是怎么确定扫描根路径的?

public Set parse(AnnotationAttributes componentScan, final String declaringClass) {
  ...
  Set 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));
  }
    ...
  return scanner.doScan(StringUtils.toStringArray(basePackages));
 }

这种场景的处理逻辑就在上面这段源码中。当basePackages是空的时候,使用declaringClass的包作为扫描的根路径,basePackages.add(ClassUtils.getPackageName(declaringClass))

6 总结

本文主要介绍了Spring IoC容器中,@ComponentScan的使用方法,以及背后的实现逻辑。

 

你可能感兴趣的:(java,spring,spring,boot)