Engineering
Glossvation Engineering
Web・モバイル

Spring Bean ライフサイクル — スコープ・初期化順・落とし穴

BeanDefinition のロードから @PostConstruct まで、Spring Bean がどの順序で生成されるかを 7 ステップで整理。Singleton/Prototype/Request/Session スコープの違いと実務的な使い分けも。

Glossvation ·

Spring Bean 生成順序

なぜ Bean ライフサイクルを知る必要があるのか

普段の開発では @Service や @Component を書けば Spring が自動で面倒を見てくれるので、ライフサイクルを意識する機会は多くありません。しかし以下のような場面で 知らないとハマる 事象に出会います:

  • 起動が遅い → どの Bean が時間を食っているか特定したい
  • @Value の値が null → @PostConstruct より前にアクセスしていた
  • 循環依存エラー → Setter 注入か @Lazy で解けるか判断したい
  • Request スコープ Bean が Singleton Controller で動かない → プロキシが要る

本稿では Bean 生成の 7 ステップ と スコープごとの挙動 を実務目線で整理します。

Bean 生成の 7 ステップ

1. BeanDefinition のロード

@Configuration / @Component / @Service / @Repository 等が付いたクラスをスキャンし、 「Bean の設計図」 (BeanDefinition) を BeanFactory に登録します。

この段階ではまだインスタンスは作られません。「このクラスは Bean です」という情報だけ集める段階。

2. BeanDefinition の後処理 (BeanFactoryPostProcessor)

BeanFactoryPostProcessor 実装が BeanDefinition を書き換えます。代表例:

  • @ConfigurationProperties のバインド準備
  • @Value で解決する ${...} プレースホルダのソース設定
  • カスタム BeanDefinition の改変 (例: BeanNameGenerator 差し替え)

3. Bean インスタンスの生成

ここで 実際の new が走ります。コンストラクタ注入ならこの時点で依存する Bean も先に解決されます。 Singleton Bean なら 1 回だけ、Prototype なら呼ばれるたびに走ります。

4. Setter / Field への依存注入

Setter 注入や @Autowired フィールドへの代入がここで起きます。 コンストラクタ注入はステップ 3 の時点で済んでいるため、循環依存に強い理由はこれです。

5. Aware 系インタフェースの処理

BeanNameAware#setBeanName, ApplicationContextAware#setApplicationContext など、 コンテナ内部の情報を Bean に渡すコールバックが呼ばれます。

6. BeanPostProcessor の前処理 → @PostConstruct → 後処理

順番は:

  1. BeanPostProcessor#postProcessBeforeInitialization
  2. @PostConstruct または InitializingBean#afterPropertiesSet
  3. カスタム init-method
  4. BeanPostProcessor#postProcessAfterInitialization (AOP プロキシ生成はここ)

@Autowired 済みのフィールドを使って初期化ロジックを走らせたいなら @PostConstruct を使う のが定石。コンストラクタでは依存先がまだ null のケースがあります (フィールド注入時)。

7. 利用可能状態 → 破棄時に @PreDestroy

コンテナ終了時 (Spring Boot なら JVM 終了シグナル時) に @PreDestroy が呼ばれます。 DB コネクションプール等の解放処理を書くのに使用。

スコープの使い分け

Scope挙動代表的ユースケース
singleton (既定)コンテナに 1 インスタンスサービス層・リポジトリ・設定
prototypegetBean() 毎に新規作成ステートフルな計算器、使い捨てビルダー
requestHTTP リクエスト毎に新規 (Web)リクエストコンテキスト、監査用トランザクション ID
sessionHTTP セッション毎に新規 (Web)ログインユーザー情報
applicationServletContext 毎に 1アプリケーション全体の一時メトリクス
websocketWS セッション毎に新規WS コネクション状態

デフォルト singleton の意味: Spring 的 Singleton = コンテナ内 1 インスタンス。 GoF Singleton (JVM プロセス内 1 インスタンス) とは別概念。 詳しくは Singleton パターン の記事参照。

よくあるハマりポイント

1. Singleton Controller から Request スコープ Bean を直接使うと壊れる

@RestController
public class MyController {
    @Autowired RequestContext ctx; // Request スコープ
    // ... ctx を使うと NullPointerException / Scope not active
}

対策: スコープドプロキシ を有効化:

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext { ... }

これで Controller には 「プロキシ」 が注入され、実際の呼び出し時に該当リクエスト用のインスタンスを引き当ててくれます。

2. @PostConstruct で @Transactional が効かない

@PostConstruct は AOP プロキシが作られる 前 に呼ばれるため、@Transactional も @Async も効きません。 対策: 初期化をリスナー (ApplicationReadyEvent) に移す:

@EventListener(ApplicationReadyEvent.class)
@Transactional
public void init() { ... }

3. コンストラクタ注入での循環依存

A が B を、B が A をコンストラクタで要求すると起動失敗 (BeanCurrentlyInCreationException)。 Setter 注入か @Lazy で遅延解決するか、そもそも設計を見直す のが正道 (循環依存は責務分離の失敗サイン)。

4. @Value の値が初期化時に空文字

Setter 注入時のタイミングで依存先 Bean より遅く解決されるケース、プロパティファイルがロードされる前に参照しているケースなど。 @PostConstruct で参照するのが安全。

デバッグに便利な小ワザ

  • 起動時に Bean 一覧を見たい → ApplicationContext#getBeanDefinitionNames() を dump
  • どの Config が Bean を提供したか → ConditionEvaluationReport を DEBUG ログで有効化
  • 遅い Bean を特定 → Spring Boot 3 の ApplicationStartup (BufferingApplicationStartup を注入) で Bean 単位の起動時間計測

まとめ: Spring Bean のライフサイクルは一見複雑ですが、「スキャン → DI → 初期化 → 利用 → 破棄」 という大枠は変わりません。細部で引っかかるポイントは 「AOP プロキシ生成のタイミング」 と 「スコープの違い」 に集約されます。この 2 つだけ押さえておけば、ほとんどのハマりは避けられます。

関連記事