Spring Initializrでプロジェクトを作ると、mainクラスに @SpringBootApplication が付いていますよね。「これを付けると動く」とは知っていても、中身が何をしているかは意外と説明しづらいものです。

結論から言うと、@SpringBootApplicationは @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan の3つを束ねた合成アノテーションです。この3つを押さえておくと、「mainクラスを移動したらBeanが見つからない」「不要な自動構成を外したい」といった場面で、どの属性を触ればいいかが見えてきます。

SpringとSpring Bootの関係がまだ曖昧な方は、先にSpringとSpring Bootの違いを読んでおくと理解しやすいと思います。

動作確認環境は次の通りです。

  • Spring Boot 3.5.x
  • Java 21
  • Gradle(Kotlin DSL)

@SpringBootApplicationとは?3つのアノテーションを束ねた合成アノテーション

まずはInitializrが生成するmainクラスです。

package com.example.app;

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

SpringApplication.run に渡している DemoApplication.class が設定の起点です。このクラスに付いている@SpringBootApplicationの定義を覗いてみましょう。Spring Boot 3.x系のソースから要点を抜粋したものです(nameGeneratorなど一部の属性は省略)。

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

    @AliasFor(annotation = EnableAutoConfiguration.class)
    Class<?>[] exclude() default {};

    @AliasFor(annotation = EnableAutoConfiguration.class)
    String[] excludeName() default {};

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

    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};

    @AliasFor(annotation = Configuration.class)
    boolean proxyBeanMethods() default true;
}

アノテーション定義の上に、さらに3つのアノテーションが付いていますよね。こうして他のアノテーションを組み合わせて作ったものを合成アノテーション(メタアノテーション)と呼びます。属性は @AliasFor で元のアノテーションに転送されているだけなので、実体は次の表の通りです。

アノテーション役割触れる属性
@SpringBootConfigurationmainクラスを設定クラスとして扱うproxyBeanMethods
@EnableAutoConfigurationクラスパスから推測して自動構成するexclude / excludeName
@ComponentScanmainクラスのパッケージ以下からBeanを探すscanBasePackages / scanBasePackageClasses

@SpringBootConfigurationはmainクラスを設定クラスにする

@SpringBootConfigurationのソースはシンプルで、@Configurationに印を1つ足しただけです。

@Configuration
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(annotation = Configuration.class)
    boolean proxyBeanMethods() default true;
}

つまりmainクラスは普通の@Configurationクラスでもあるので、@Beanメソッドを直接書くこともできます。動きはしますが、実務では別の@Configurationクラスに分けるのが一般的です。使い分けは@Configuration / @Beanって何?にまとめています。

ではなぜ専用のアノテーションがあるのかというと、テストのためです。@SpringBootTest はテストクラスのパッケージから上に向かって@SpringBootConfigurationの付いたクラスを探し、設定クラスとして使います。classes を指定しなくても動くのはこの印があるからなんですね。詳しくはwebEnvironmentの使い分けで扱っています。

@EnableAutoConfigurationはクラスパスから設定を推測する

spring-boot-starter-web を依存に足しただけで、組み込みTomcatもDispatcherServletも用意されますよね。あの「何も設定していないのに動く」現象の入口が@EnableAutoConfigurationです。クラスパスにあるライブラリと登録済みのBeanを見て、「この状況ならこの設定が必要だろう」と判断し、設定クラスを自動で追加してくれます。

Starterが依存関係を揃え、自動構成がそれを設定する、という役割分担です。Starterの中身はSpring Boot Starterって何?を、「必要かどうか」を判断する内部機構は自動構成(AutoConfiguration)の仕組みを参照してください。この記事では後述するexcludeでの制御に絞ります。

@ComponentScanはmainクラスのパッケージ以下からBeanを探す

先ほどのソースを見ると、@ComponentScanには basePackages が指定されていません。この場合、Springは アノテーションが付いたクラスのパッケージ を起点に、サブパッケージまでをスキャンします。つまりmainクラスの置き場所が、そのままBean検出の範囲を決めています。

com.example.app
├── DemoApplication.java      ← ここが起点
├── controller
│   └── GreetingController.java
├── service
│   └── GreetingService.java
└── repository
    └── GreetingRepository.java

mainクラスが com.example.app 直下にあるので、controller / service / repository のすべてが範囲に入ります。検出されるのは @Component と、それを継承した @Service / @Repository / @Controller などです。@Componentそのものは@Componentって何?に譲ります。

ちなみに excludeFilters の2つは、TypeExcludeFilter がテストスライス(@WebMvcTestなど)で対象外のBeanを除外するためのフック、AutoConfigurationExcludeFilter が自動構成クラスを通常のスキャンで拾わないためのフィルタです。後半の実験で少しだけ関係してきます。

mainクラスをルート以外に置くとBeanが見つからなくなる

実際に壊してみましょう。まずServiceとControllerを用意します。

package com.example.app.service;

@Service
public class GreetingService {
    public String greet(String name) {
        return "Hello, " + name;
    }
}
package com.example.app.controller;

@RestController
public class GreetingController {
    private final GreetingService greetingService;

    public GreetingController(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    @GetMapping("/greet")
    public String greet() {
        return greetingService.greet("Spring");
    }
}

この状態でmainクラスを com.example.app.config に移動します。

package com.example.app.config;

@SpringBootApplication
public class DemoApplication {

    @Bean
    CommandLineRunner runner(GreetingService greetingService) {
        return args -> System.out.println(greetingService.greet("Spring"));
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

起動すると、次のように落ちます。

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of method runner in com.example.app.config.DemoApplication required a bean of type 'com.example.app.service.GreetingService' that could not be found.

Action:

Consider defining a bean of type 'com.example.app.service.GreetingService' in your configuration.

スキャンの起点が com.example.app.config になったため、service も controller も範囲外になったわけです。少し怖いのはControllerだけが範囲外になったケースで、誰もControllerを注入していなければ起動は成功し、/greet が404を返すだけなので原因に気づきにくいんですよね。

解決策はシンプルで、mainクラスをルートパッケージ com.example.app に戻すだけです。次のscanBasePackagesはあくまで例外的な手段と考えてください。なお「APPLICATION FAILED TO START」はポート衝突やBean重複でも出るので、切り分け方はSpring Bootアプリが起動しない時の原因切り分けを参考にしてください。

scanBasePackages / scanBasePackageClasses でスキャン範囲を変える

どうしてもmainクラスを動かせない場合は、スキャン範囲を明示できます。

// 文字列で指定
@SpringBootApplication(scanBasePackages = "com.example.app")
public class DemoApplication { ... }

// クラス参照で指定(こちらを推奨)
@SpringBootApplication(scanBasePackageClasses = AppRoot.class)
public class DemoApplication { ... }

scanBasePackageClasses には起点にしたいパッケージにあるクラスを渡します。パッケージ名の書き間違いにコンパイルで気づけますし、IDEのリファクタリングにも追従するので、使うならこちらを優先しましょう。

注意点として、scanBasePackagesを指定するとデフォルトの「mainクラスのパッケージ」は起点から 外れます。mainクラスの隣に置いたBeanが急に見つからなくなることがあります。

判断基準はこうです。

  • 原則はmainクラスをルートパッケージに置く。それだけで属性は不要
  • マルチモジュールで別モジュールのパッケージを含めたい、既存の構成を動かせない、という場合だけscanBasePackageClassesを使う

マルチモジュールや明示的な@ComponentScanを使うパターンは@ComponentScanでBeanが見つからない問題の解決方法で網羅しています。

exclude / excludeName で不要な自動構成を外す

次は@EnableAutoConfiguration側の属性です。よくあるのが、spring-boot-starter-data-jpa を入れたもののDBをまだ用意していなくて、起動時に Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. で落ちるケースです。一時的に外したいなら、excludeで自動構成クラスを指定します。

// クラス参照で除外(通常はこちらで十分)
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class DemoApplication { ... }

// FQCNの文字列で除外
@SpringBootApplication(excludeName =
        "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration")
public class DemoApplication { ... }

これで起動が通ります。ただし、この除外が有効なのは、まだ @Entity や JpaRepository を使っていない段階に限ります。リポジトリを注入している箇所があれば今度はそのBeanが見つからず失敗するので、DBを用意するまでの一時しのぎと考えてください。

excludeはコンパイル時にクラスが存在する必要があります。excludeName はクラスパスに無いかもしれない自動構成を指定するためのもので、出番は共通ライブラリなど「どの依存が入っているか分からない」場面に限られます。

もう1つの選択肢がプロパティでの指定です。プロファイルごとに切り替えたいなら、アノテーションよりこちらが扱いやすいです。

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
# 次のセクションで使う自動構成レポートの出力
debug=true

—debugで自動構成レポートを読む

自動構成が「なぜ有効になったか、なぜ無効になったか」は、起動時に --debug を付けると全部出力されます。先ほどの debug=true でも同じです。

./gradlew bootRun --args='--debug'
# jarで起動する場合(jar名はプロジェクト名とバージョンに合わせて読み替えてください)
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --debug

起動ログの中に「CONDITIONS EVALUATION REPORT」という見出しが出てくるので、そこを探してください。

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   DispatcherServletAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
      - found 'session' scope (OnWebApplicationCondition)


Negative matches:
-----------------

   BatchAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.springframework.batch.core.launch.JobLauncher' (OnClassCondition)


Exclusions:
-----------

    org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration


Unconditional classes:
----------------------

    org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

レポートは4つのセクションに分かれていて、主に見るのは最初の3つです。

  • Positive matches は条件を満たして有効化された自動構成です。DispatcherServletクラスが見つかったのでMVCの自動構成が有効になっています
  • Negative matches は無効化された自動構成とその理由です。Spring Batchを入れていないのでBatchAutoConfigurationが外れています
  • Exclusions はexcludeやプロパティで明示的に外したものです。先ほどのDataSourceAutoConfigurationがここに出ていますね
  • Unconditional classes は条件なしで常に適用される自動構成です

読み方のコツは、「期待した自動構成が効いていない」ときにまずNegative matchesで該当クラス名を検索し、did not find の理由を見ることです。条件アノテーションの意味は自動構成の仕組みで解説しています。

実験として3つのアノテーションに書き換えてみる

ここまで来たら、@SpringBootApplicationを外して3つを直接並べてみましょう。mainクラスはルートパッケージに戻してあります。

package com.example.app;

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class DemoApplication {

    @Bean
    CommandLineRunner printBeans(ApplicationContext context) {
        return args -> Arrays.stream(context.getBeanDefinitionNames())
                .filter(n -> n.contains("greeting") || n.contains("dispatcherServlet"))
                .forEach(System.out::println);
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

起動すると、自前の greetingController と greetingService、自動構成由来の dispatcherServlet が揃って出力されます。@SpringBootApplication版に同じCommandLineRunnerを仕込んで比べても、出力は同じでした。

greetingController
greetingService
dispatcherServlet
dispatcherServletRegistration

ただし完全に同じではなく、違いが出るのはテストです。@Configurationには@SpringBootTestが探す「印」がないので、このmainクラスは自動検出されず @SpringBootTest(classes = DemoApplication.class) と明示する必要があります。excludeFiltersの2つのフィルタを省いているので、テストスライスでの除外が効かなくなる可能性もあります。

この実験は合成アノテーションの中身を理解するためのもので、本番コードでは素直に@SpringBootApplicationを使うのが正解です。

proxyBeanMethods属性について

表に出てきたproxyBeanMethods(デフォルトtrue)は、@Configurationの同名属性にそのまま転送され、mainクラスにCGLIBプロキシをかけるかどうかを制御します。falseにすると起動が僅かに軽くなりますが、mainクラス内で@Beanメソッド同士を呼び合うと別インスタンスになる副作用があります。通常はデフォルトのままで問題ありません。CGLIBプロキシの挙動は@Configuration / @Beanの記事で詳しく扱っています。

まとめ

@SpringBootApplicationは、3つのアノテーションの属性をまとめて触れる窓口でした。

構成要素対応する属性使う場面
@SpringBootConfigurationproxyBeanMethods基本は触らない
@EnableAutoConfigurationexclude / excludeName不要な自動構成を外したい
@ComponentScanscanBasePackages / scanBasePackageClassesmainクラスを動かせない

迷ったときの判断基準は次の4つです。

  • mainクラスはルートパッケージに置く
  • scanBasePackageClassesは構成を動かせないときだけ
  • 自動構成の除外はexcludeか spring.autoconfigure.exclude
  • 効いているか分からなければ --debug でレポートを見る

「おまじない」だったアノテーションが、属性の意味まで追える道具に変わっていれば嬉しいです。自動構成の条件判断を深く知りたい方は自動構成の仕組みへ、マルチモジュールでBeanが見つからない方は@ComponentScanでBeanが見つからない問題の解決方法へ進んでみてください。