Spring Bean ライフサイクル — スコープ・初期化順・落とし穴
BeanDefinition のロードから @PostConstruct まで、Spring Bean がどの順序で生成されるかを 7 ステップで整理。Singleton/Prototype/Request/Session スコープの違いと実務的な使い分けも。

なぜ 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 → 後処理
順番は:
BeanPostProcessor#postProcessBeforeInitialization@PostConstructまたはInitializingBean#afterPropertiesSet- カスタム
init-method BeanPostProcessor#postProcessAfterInitialization(AOP プロキシ生成はここ)
@Autowired 済みのフィールドを使って初期化ロジックを走らせたいなら @PostConstruct を使う のが定石。コンストラクタでは依存先がまだ null のケースがあります (フィールド注入時)。
7. 利用可能状態 → 破棄時に @PreDestroy
コンテナ終了時 (Spring Boot なら JVM 終了シグナル時) に @PreDestroy が呼ばれます。
DB コネクションプール等の解放処理を書くのに使用。
スコープの使い分け
| Scope | 挙動 | 代表的ユースケース |
|---|---|---|
singleton (既定) | コンテナに 1 インスタンス | サービス層・リポジトリ・設定 |
prototype | getBean() 毎に新規作成 | ステートフルな計算器、使い捨てビルダー |
request | HTTP リクエスト毎に新規 (Web) | リクエストコンテキスト、監査用トランザクション ID |
session | HTTP セッション毎に新規 (Web) | ログインユーザー情報 |
application | ServletContext 毎に 1 | アプリケーション全体の一時メトリクス |
websocket | WS セッション毎に新規 | 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 つだけ押さえておけば、ほとんどのハマりは避けられます。
関連記事
- Web・モバイル·
Spring Boot プロジェクト構成 — 10 人規模で崩れないパッケージ設計
Controller / Service / Repository の責務境界、ドメイン層の切り方、テストの並べ方まで。Spring Boot API 開発で「後から直せなくなるコード」を防ぐためのパッケージ構成の指針。
- Web・モバイル·
Spring AOP 実装パターン — トランザクション・ログ・権限を本流から剥がす
横断的関心事 (クロスカッティングコンサーン) を綺麗に分離する Spring AOP の使い所と Aspect/JoinPoint/Advice/Pointcut の全体像、プロダクションで効くユースケース 4 選。
- Web・モバイル·
Java パフォーマンスチューニングの実務ノート — JVM / コレクション / GC
プリミティブ型・不変オブジェクト・コレクション選択・ストリーム vs for ループ・GC チューニングまで、現場で効くチューニングの型。計測してからチューニングする原則を軸に。