Spring Bootアプリの起動が遅いと、ローカル開発でリビルドのたびに集中力が切れますし、Kubernetes上ではPodがreadiness probeに間に合わずに落とされる、なんてこともありますよね。本記事では GraalVM Native Image までは踏み込まずに、通常のJVM環境のままで起動を短くする手順をまとめました。

起動が遅くなる主な原因を押さえる

まずは何に時間がかかっているかの当たりを付けましょう。Spring Bootの起動で重くなりがちなのは、だいたい次のあたりです。

  • ClasspathスキャンとAutoConfigurationの条件評価
  • 多すぎる spring-boot-starter-* 依存
  • DataSourceやHibernateの初期化、外部接続待ち
  • 広すぎる @ComponentScan 範囲

推測で手を入れる前に、計測から始めるのが鉄則です。

—debug と ApplicationStartup で計測する

手っ取り早く全体像を見るには --debug オプションが便利です。

java -jar app.jar --debug

これで CONDITIONS EVALUATION REPORT が出力され、どのAutoConfigurationが有効で、なぜ無効になったかが分かります。

さらにステップ単位で時間を取りたいときは BufferingApplicationStartup を仕込みます。

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MyApplication.class);
    app.setApplicationStartup(new BufferingApplicationStartup(2048));
    app.run(args);
}

バッファに各ステップの所要時間が記録されるので、後述の Actuator から取り出せます。Actuatorを入れずに自前で読みたい場合は FlightRecorderApplicationStartup をJFRに流す手もあります。

Actuator startup エンドポイントで可視化

spring-boot-starter-actuator を入れて、startup エンドポイントを公開します。

management.endpoints.web.exposure.include=startup

起動後に GET /actuator/startup を叩くとJSONでステップ一覧が返ってきます。timeline.events[].duration を降順で並べれば、ボトルネックの本命がすぐ見えます。本番で公開する場合は、必ず認可で保護するか、内部ネットワーク限定にしてください。

spring.main.lazy-initialization の効果と落とし穴

もっとも手軽な施策が遅延初期化です。

spring.main.lazy-initialization=true

全Beanの生成が初回利用時まで遅延されるので、起動だけ見れば確実に速くなります。ただし副作用もあって、

  • 初回リクエストのレイテンシが悪化する
  • 設定ミスによる起動時エラーが、本番の最初のリクエストまで顕在化しない

といった落とし穴があります。ローカル開発やCIでは積極的に有効化、本番では @Lazy で個別のBeanに絞る、という使い分けが現実的です。

不要な AutoConfiguration を除外する

条件評価レポートの Positive matches を眺めて、使っていない機能を見つけたら除外します。

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration;

@SpringBootApplication(exclude = {
    JmxAutoConfiguration.class,
    WebSocketServletAutoConfiguration.class
})
public class MyApplication { }

プロパティでまとめて指定する書き方もあります。

spring.autoconfigure.exclude=\
  org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration

JMX、WebSocket、Mail、Redis あたりは「依存に入っているが使っていない」典型例です。除外したら必ず回帰テストを通して、想定外のBeanが消えていないか確認しましょう。

コードで一元管理したいなら @SpringBootApplication(exclude=...)、プロファイルや環境変数で切り替えたいなら spring.autoconfigure.exclude、と用途で使い分けるのがおすすめです。AutoConfigurationの仕組み自体は Spring BootのAutoConfigurationの仕組み もどうぞ。

Component スキャンと依存関係を整理する

@SpringBootApplication をルートパッケージに置きっぱなしにしていると、配下の全パッケージをスキャンします。basePackages を明示するだけでも、それなりに効きます。

@SpringBootApplication(scanBasePackages = "com.example.app.api")
public class MyApplication { }

あわせて pom.xmlbuild.gradle の Starter依存を棚卸しして、未使用のものを外しておくと土台から軽くなります。

Spring Boot 3.3+ の CDS を有効化する

JVM標準の Class Data Sharing は、クラスメタデータを事前に共有アーカイブ化することで読み込みを高速化します。Spring Boot 3.3 から正式にサポートされました。

手順は2段階です。まず1回トレーニングランを実施してアーカイブを作ります。

java -XX:ArchiveClassesAtExit=app.jsa \
     -Dspring.context.exit=onRefresh \
     -jar app.jar

java -XX:SharedArchiveFile=app.jsa -jar app.jar

-Dspring.context.exit=onRefresh は、Applicationコンテキストの refresh が終わった直後にプロセスを終了させるためのオプションです。実サービスを起動せずアーカイブだけ生成したいので、トレーニングランではこれを指定します。

短縮率はクラス数やJDKバージョンに左右されるので断言は難しいですが、Spring公式ブログなどでも二桁%程度の改善報告があり、副作用がほぼないため優先度の高い施策です。Dockerで使う場合は、生成した app.jsa をイメージに含めておくのを忘れずに。

GraalVMを使わない AOT モード

Spring Boot は GraalVM Native Image を使わなくても、AOT処理だけを通常JVMで活用できます。Beanのメタデータを事前生成し、起動時の反射処理を減らす仕組みです。

実行時は次のプロパティを有効化します。

spring.aot.enabled=true

ただしこれ単体では効きません。ビルド時に Maven なら spring-boot-maven-pluginprocess-aot ゴール、Gradle なら processAot タスクを実行して、AOT成果物を生成しておく必要があります。

./mvnw spring-boot:process-aot
# Gradleの場合
./gradlew processAot

AOTを有効化すると、プロファイルなど一部の設定が起動時ではなくビルド時に確定されるため、実行時にプロファイルを切り替える運用とは相性が悪い点に注意してください。CDS と組み合わせれば効果が積み上がるので、本番向けには両方有効化するのが王道です。Native Image との違いについては Spring BootでGraalVM Native Imageを使う方法 も参考にしてください。

Kubernetes では startup probe を活用する

それでも起動に数十秒かかる場合は、Kubernetes側で猶予を確保しましょう。startupProbe を設定すると、起動完了までは livenessProbe の判定が抑制されます。

startupProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  failureThreshold: 30
  periodSeconds: 5
readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  periodSeconds: 10
livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  periodSeconds: 10

Actuator の /health/liveness/health/readiness を活用すれば、Spring Boot の起動完了状態を素直にprobeに反映できます。Actuator自体の使い方は Spring Boot Actuator入門 を参照してください。

計測 → 施策 → 再計測のサイクルを回す

最後に進め方の話です。施策を一気に入れると、何がどれくらい効いたのか分からなくなるので、必ず1つずつ適用します。経験的に効果の大きい順は次のあたりです(カッコ内は本文中の対応見出し)。

  1. 依存整理と Component スキャン範囲の絞り込み(「Component スキャンと依存関係を整理する」)
  2. 不要な AutoConfiguration の除外(「不要な AutoConfiguration を除外する」)
  3. CDS と AOT の併用(「Spring Boot 3.3+ の CDS」「GraalVMを使わない AOT モード」)
  4. 遅延初期化(「spring.main.lazy-initialization」、主にローカル・CI向け)

CDS/AOT を遅延初期化より上位に置いているのは、本番でも副作用が少なく定常的に効くためです。ただしアプリ構成によって順位は前後するので、ベースラインは複数回計測して平均を取り、1施策ずつ効果を確かめるのが結局いちばん近道です。

まとめ

起動高速化は派手な単一施策よりも、計測ベースで小さな改善を積み上げるのが結局いちばん効きます。まずは Actuator の startup エンドポイントでボトルネックを見える化し、依存整理 → AutoConfig除外 → CDS/AOT の順で試してみてください。GraalVM に踏み込まなくても、十分実用的な短縮が狙えます。