APPLICATION FAILED TO START という赤い文字を見て、どこから手を付けていいか分からなくなることはありませんか。Spring Bootの起動失敗エラーは一見こわいですが、実はパターンがある程度決まっています。この記事ではSpring Boot 3.x / Java 17+ を前提に、原因を分類しながらログから対処にたどり着く流れを紹介します。

なお、起動はするが遅い、というケースは別物です。その場合は Spring Bootの起動高速化 を参照してください。

起動失敗の全体像をつかむ

Spring Bootが起動に失敗すると、まず FailureAnalyzer が整形した分かりやすいメッセージを出してくれます。その下にスタックトレースが続く、という二段構えです。

よく出会う原因は、だいたい次の7つに集約されます。

  • ポート衝突
  • Bean定義の重複・あいまい解決
  • 循環参照
  • AutoConfigurationの失敗
  • DataSource設定漏れ
  • @ConfigurationProperties のバリデーション失敗
  • プロファイル設定ミス

切り分けの基本は、整形メッセージ → スタックトレース → --debug 出力、の順で見ていくことです。ほとんどのケースは最初の整形メッセージだけで原因が分かります。

FailureAnalyzerメッセージの読み方

FailureAnalyzerが出力するブロックは、Description 欄と Action 欄に分かれています。

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

Description:

Web server failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

Description は「何が起きたか」、Action は「次に何をすべきか」を示しています。Action欄の指示がそのまま解決の最短ルートになるケースが多いので、まずはここを素直に読みましょう。

なお、FailureAnalyzerが対応していないエラーの場合は整形メッセージが出ず、スタックトレースだけになります。その時は Caused by: を下から順にたどって、最初に出てくる業務クラスや設定ファイル名を見つけるのがコツです。

—debugフラグでもっと情報を引き出す

原因が見えない時は詳細ログを出します。実行可能JARなら --debug をそのまま渡せます。

java -jar app.jar --debug

MavenやGradle経由で起動するときは、フラグの渡り方がバージョン依存で揺れるので、application.properties に書くか JVM プロパティで指定するのが確実です。

# application.properties
debug=true
# JVMシステムプロパティで指定する場合
./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-Ddebug"
./gradlew bootRun --args='--debug'

すると起動ログに CONDITIONS EVALUATION REPORT が出ます。Positive matches に適用されたAutoConfiguration、Negative matches に適用されなかった理由が並ぶので、期待どおりに自動設定が効いていない時の原因が見つけやすくなります。仕組みは Spring Boot AutoConfigurationの仕組み で詳しく解説しています。

ポート衝突

一番遭遇しやすいのがポート衝突です。原因は単純で、すでに別のプロセスが8080を使っています。

# macOS / Linux
lsof -i:8080

# Windows
netstat -ano | findstr 8080

前回のSpring Bootアプリが残っていたり、Dockerコンテナが動いていたり、ということがよくあります。原因のプロセスを停止できれば解決です。

どうしても止められない時は、application.properties でポートを変えてしまいます。テスト用にランダムポートを使いたいときは 0 を指定するのが便利です。

server.port=8081
# テスト時は server.port=0 でランダム割当

Bean定義の重複・あいまい解決

次によく見るのが BeanDefinitionOverrideExceptionNoUniqueBeanDefinitionException です。名前が似ていて混乱しがちですが、原因は違います。

  • BeanDefinitionOverrideException は、同じ名前のBeanが2つ以上登録された場合
  • NoUniqueBeanDefinitionException は、同じ型のBeanが複数あって注入先で1つに絞れない場合

後者は @Primary@Qualifier で対処できます。

@Service
public class OrderService {

    private final PaymentGateway gateway;

    public OrderService(@Qualifier("stripeGateway") PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

前者については spring.main.allow-bean-definition-overriding=true で抑えることもできますが、これは応急処置です。Spring Boot 2.1以降は意図しない上書きを防ぐためにデフォルトが false に変更された経緯があり、有効にすると後勝ちで黙ってBeanが差し替わるリスクを抱えます。根本原因はだいたいコンポーネントスキャン範囲のかぶりなので、設計を見直したほうが安全です。

循環参照(Circular reference)

Spring Boot 2.6以降、循環参照はデフォルトで禁止されました。AがBに依存し、BがAに依存するような状態だと起動時にエラーになります。

The dependencies of some of the beans in the application context form a cycle:

┌─────┐
|  serviceA defined in file [...]
↑     ↓
|  serviceB defined in file [...]
└─────┘

ログにこの経路図が出るので、依存の輪っかが一目で分かります。対処は主に3パターンです。

1つ目は setter 注入に切り替える方法。コンストラクタ注入では循環があると初期化できませんが、setter 注入ならBean生成後に解決されます。

@Service
public class ServiceA {

    private ServiceB serviceB;

    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

2つ目は @Lazy を片側に付けてプロキシ経由で遅延解決する方法。

@Service
public class ServiceA {

    private final ServiceB serviceB;

    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

3つ目は設計を見直して責務を分離する方法で、共通処理を第三のサービスに切り出すと循環がほどけます。長期的にはこれが一番健全です。

どうしても急ぎなら spring.main.allow-circular-references=true で再び循環を許可することもできますが、初期化順が読みにくくなる副作用があるので、暫定対処と割り切りましょう。Bean関連の話題は Bean Lifecycleの記事 も参考になります。

AutoConfigurationが期待通り動かない

「依存を入れたのに自動設定が効かない」時は、--debug 出力の Negative matches を見ましょう。

HibernateJpaAutoConfiguration:
   Did not match:
      - @ConditionalOnClass did not find required class
        'org.hibernate.SessionFactory' (OnClassCondition)

この例では spring-boot-starter-data-jpa 相当の依存が抜けているのが原因と推測できます。「このクラスが見つからなかった」「このBeanがすでに存在した」など、条件が満たされなかった理由が書かれているので、starter依存の抜けやバージョン不整合に気づきやすくなります。

逆に、特定のAutoConfigurationを意図的に止めたい時は exclude を使います。

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

DataSource設定漏れ

DataSource クラスがクラスパス上にあるのに接続URLが指定されておらず、H2などの組み込みDBドライバも見つからない、というときに出るのがこれです。

Failed to configure a DataSource: 'url' attribute is not specified
and no embedded datasource could be configured.

spring-jdbcspring-boot-starter-data-jpa を入れただけでJDBCドライバを追加し忘れている、本番ではPostgreSQLを使うのにdev環境で application.properties を書いていない、といったケースで遭遇します。

DBを使うなら、最低限これだけ設定すればOKです。

spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=app
spring.datasource.password=secret

逆に、このアプリではDBを使わない(ライブラリの都合で依存だけ入っている)場合は、先ほどの excludeDataSourceAutoConfiguration を外します。コネクションプールの詳しい設定は HikariCPチューニングガイド を参照してください。

@ConfigurationPropertiesのバリデーション失敗

設定値の型が合わない、あるいは @Validated で付けた制約に違反した時も起動が止まります。

Binding to target ... failed:
   Property: app.maxRetries
   Value: "abc"
   Reason: failed to convert java.lang.String to int

メッセージに PropertyValue がそのまま出るので、application.properties の該当キーを直せば解決します。キー名のタイポや、@NotNull を付けたフィールドに値が入っていないパターンが定番です。

プロファイル設定ミスによる起動失敗

設定値そのものは書いてあるのに、プロファイルが噛み合わず読み込まれない、というのも見落としがちです。

アクティブプロファイルは次のいずれかで指定します。

# 環境変数
export SPRING_PROFILES_ACTIVE=dev

# JVM引数
java -jar app.jar -Dspring.profiles.active=dev
# application.properties
spring.profiles.active=dev

命名規則は application-{profile}.properties で、dev を指定すれば application-dev.properties が読み込まれます。共通の application.properties が先に読まれ、プロファイル別ファイルが上書きする順番です。

ありがちなのは、本番接続用の spring.datasource.urlapplication-prod.properties に書いたまま SPRING_PROFILES_ACTIVE を設定し忘れ、起動時にURLが null になって Failed to configure a DataSource が出るパターン。起動ログの冒頭にある The following profiles are active: を必ず確認するクセを付けましょう。

それでも分からない時は

最終手段として、最小再現環境を作って切り分けます。具体的にはこの3つです。

  • logging.level.org.springframework=DEBUG でログを増やす
  • 関係なさそうなBean・設定を一時的に外す
  • Spring Boot / Java / DBドライバのバージョンを明示する

この形にして再現すれば、自分でも気づきやすくなりますし、社内チャットやStack Overflowで質問する時にも回答が得やすくなります。

まとめ

起動失敗エラーは、まず DescriptionAction を読む。それで足りなければ --debug で詳細を出す。あとは原因カテゴリごとの典型パターンを思い出す、という流れで大体解決できます。慌てずに整形メッセージから順に追っていくのが、結局は一番の近道です。