Spring bootのコンポーネントスキャンのソースコードリーディングのログ

- (9 min read)

spring bootを使うにあたってコンポーネントスキャン周りを理解したかった

登場人物

  • コンポーネント: アプリケーションのパーツ(リポジトリの実装とかアルゴリズムの実装とか)
    • Springの世界ではDIコンテナで管理されているコンポーネントはBeanと呼ばれる(Bean定義じゃなくてBean実装)
  • DIコンテナ: コンポーネントを登録する場所
    • Configuration: コンポーネントをDIコンテナに登録するときに使用される設定ファイル
      • Springの世界ではBean定義と呼ばれる
        • コンポーネントを実装しているコードをBean定義と呼べばいいのにと思ったが、beanはmetadataとともに生成されるとのことで、コンポーネント(POJO)をDIコンテナから使えるようにbeanとして定義するconfigurationのことをBean定義と呼ぶと理解した。
    • ApplicationContext: DIコンテナを触るためのインターフェース
  • アプリケーション: コンポーネントの利用者
    • DIコンテナからBeanをルックアップする

spring bootのエントリーポイント

// SpringBootMyApplication.java
@SpringBootApplication
public class SpringBootMyApplication {
  public static void main(String[] args) {
    SpringApplication.run(SpringBootMyApplication.class, args);
  }
}

spring bootでは@SpringBootApplicationアノテーションが重要な役割を持つ(ソース)

// SpringBootApplication.java
// 省略
@SpringBootConfiguration
@ComponentScan
// 省略
public @interface SpringBootApplication {
  // 省略
}

@SpringBootApplication@ComponentScanを継承している。@CompnenntScanはSpringで定義されており、はvalue属性に指定したパッケージ(デフォルトでは付与されたクラスと同じパッケージ)配下のコンポーネントをスキャンしてBeanとして登録できる。たとえば、以下のように書く

@Configuration // configファイルであることの宣言
@ComponentScan("com.myexmple.app") // コンポーネントスキャン
public class AppConfig {
  @Bean // メソッド名: bean名, 返り値: beanのインスタンス
  MyRepository myRepository () {
    return new MyRepositoryImpl();
  }
  @Bean
  MyService myService(MyRepository myRepository) { // 他のコンポーネントを注入できる
    return new MyServiceImpl(myRepository);
  }
  @Bean
  MyService myService2 () {
    return new MyServiceImpl2(); // 同じインターフェースを満たした違う実装
  }
}

ConfigurationはBean定義でもあるので@Beanというアノテーションを付けるとコンポーネントスキャンしたファイルをBeanとしてDIコンテナに登録してくれるが、Spring BootではコンポーネントのPOJOに@Componentのようなアノテーションを付けることでConfiguration側で@Beanを用いたメソッドを作らなくてもDIコンテナに登録されるようになる。(この仕組はSpringのもの)

@ComponentScanの動きを見ていく

まずエントリーポイントでSpringApplication.run(SpringBootMyApplication.class, args);が実行される ソース

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
        return run(new Class<?>[] { primarySource }, args);
}

上記のrunは以下を呼び出す

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
	return new SpringApplication(primarySources).run(args);
}

その後以下が呼ばれる

public ConfigurableApplicationContext run(String... args) {
  // 省略
  ConfigurableApplicationContext context = null;
  // 省略
  try {
    // 省略
    context = createApplicationContext();
    prepareContext(context, environment, listeners, applicationArguments, printedBanner);
    refreshContext(context);
    // 省略
  }
  // 省略
}

createApplicationContext()では ServletWebServerApplicationContextか、AnnotationConfigReactiveWebServerApplicationContextか、AnnotationConfigApplicationContextのいずれかのクラスが生成される。前者2つはSpringBootで最後の一つはSpringで定義されている。いずれもAbstractApplicationContextを継承している

refreshContextの定義は以下の通り

private void refreshContext(ConfigurableApplicationContext context) {
  refresh((ApplicationContext) context);
  // 省略
}

そしてrefreshが呼ばれる

protected void refresh(ApplicationContext applicationContext) {
  Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext);
  refresh((ConfigurableApplicationContext) applicationContext);
}

protected void refresh(ConfigurableApplicationContext applicationContext) {
  applicationContext.refresh();
}

createApplicationContext()で生成したインスタンスのクラスはいずれもAbstractApplicationContextを継承していたが、その中でrefreshは定義されている。ここでようやくSpringBootからSpring本体のコードに入る

// AbstractApplicationContext.java
public void refresh() throws BeansException, IllegalStateException {
  // 省略
  // Tell the subclass to refresh the internal bean factory.
  ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
  // 省略

  try {
    // Invoke factory processors registered as beans in the context.
    invokeBeanFactoryPostProcessors(beanFactory);
    // 省略
  }
  // 省略
}

invokeBeanFactoryPostProcessorsが呼び出される

// AbstractApplicationContext.java
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
  PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
  // 省略
}

PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessorsが呼び出される

// PostProcessorRegistrationDelegate.java
public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
  // 省略
  BeanDefinitionRegistryPostProcessor registryProcessor = (BeanDefinitionRegistryPostProcessor) postProcessor;
  registryProcessor.postProcessBeanDefinitionRegistry(registry);
  // 省略
}

ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry

// ConfigurationClassPostProcessor.java
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
  // 省略
  processConfigBeanDefinitions(registry);
  // 省略
}

processConfigBeanDefinitions

// ConfigurationClassPostProcessor.java
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
  List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
  String[] candidateNames = registry.getBeanDefinitionNames();
  for (String beanName : candidateNames) {
    // 省略
    configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
    // 省略
  }
  // 省略

  // Parse each @Configuration class
  ConfigurationClassParser parser = new ConfigurationClassParser(
    this.metadataReaderFactory, this.problemReporter, this.environment,
    this.resourceLoader, this.componentScanBeanNameGenerator, registry);
  Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
  // 省略
  parser.parse(candidates);
  // 省略
}

@Configurationの付与されたクラス(Configurationファイル)をparser.parse(candidates);でパースしている。@Configurationアノテーションは特に説明していなかったが、classに付与することでConfigurationファイルとして扱えるようにするためのアノテーションだ。@SpringBootApplication -> @SpringBootConfiguration -> @Configurationという継承関係になっている。 ConfigurationClassParser.parse

public void parse(Set<BeanDefinitionHolder> configCandidates) {
  for (BeanDefinitionHolder holder : configCandidates) {
    BeanDefinition bd = holder.getBeanDefinition();
    // 省略
    if (bd instanceof AnnotatedBeanDefinition) {
      parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
    }
    else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
      parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
    }
    else {
      parse(bd.getBeanClassName(), holder.getBeanName());
    }
    // 省略
  }
}

それぞれparseメソッドを呼び出している。これらの定義は以下

// ConfigurationClassParser.java
protected final void parse(@Nullable String className, String beanName) throws IOException {
	Assert.notNull(className, "No bean class name for configuration class bean definition");
	MetadataReader reader = this.metadataReaderFactory.getMetadataReader(className);
	processConfigurationClass(new ConfigurationClass(reader, beanName), DEFAULT_EXCLUSION_FILTER);
}

protected final void parse(Class<?> clazz, String beanName) throws IOException {
	processConfigurationClass(new ConfigurationClass(clazz, beanName), DEFAULT_EXCLUSION_FILTER);
}

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
	processConfigurationClass(new ConfigurationClass(metadata, beanName), DEFAULT_EXCLUSION_FILTER);
}

いずれにせよ、processConfigurationClassを呼び出している ConfigurationClassParser.processConfigurationClass

// ConfigurationClassParser.java
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
  // 省略
  SourceClass sourceClass = asSourceClass(configClass, filter);
  do{
    sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
  }
  while (sourceClass != null);
  // 省略
}

ConfigurationClassParser.doProcessConfigurationClass

// ConfigurationClassParser.java
protected final SourceClass doProcessConfigurationClass(
  ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
  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());
      // 省略
    }
  }
  // 省略
}

this.componentScanParser.parseComponentScanAnnotationParser.parseである ComponentScanAnnotationParser.parse

// ComponentScanAnnotationParser.java
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
  // 省略
  return scanner.doScan(StringUtils.toStringArray(basePackages));
}

ClassPathBeanDefinitionScanner.doScan

// ClassPathBeanDefinitionScanner.java
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
  // 省略
  for (String basePackage : basePackages) {
    Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
    for (BeanDefinition candidate : candidates) {
      ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
      candidate.setScope(scopeMetadata.getScopeName());
      // 省略
      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);
      }
    }
  }
  // 省略
}

registerBeanDefinitionでbean定義を登録していると思われる