はじめに
前回の記事では、DIコンテナが起動時に「①収集→②生成→③注入」という3フェーズをこなしていることを見ました。
ここで1つ疑問が残ります。@Autowired による注入が終わった瞬間、そのBeanは本当に「使える状態」になっているのでしょうか?
たとえば、次のような処理をしたい場面を考えてみてください。
- 注入された設定値を使って、キャッシュを事前に温めておきたい
- 注入されたコネクション情報を使って、DB接続を開いておきたい
- アプリ終了時に、開いたコネクションをきちんと閉じたい
これらは「依存性が注入された あと」でないとできませんし、「コンテナが破棄される 直前」に確実に実行してほしい処理です。今回は、この「注入完了後」と「破棄直前」にフックする仕組みを見ていきます。
対象読者:
- 前回・前々回の記事を読んだ人
- Beanの初期化処理をどこに書けばいいか迷ったことがある人
- アプリ終了時のリソース解放処理に不安がある人
1. Beanのライフサイクル全体像
前回図示した「収集→生成→注入」の流れに、今回のテーマである「初期化」と「破棄」を足すと、Beanの一生はこうなります。
今回説明する @PostConstruct と @PreDestroy は、まさに④と⑥、「注入の直後」と「破棄の直前」というピンポイントのタイミングにフックするためのアノテーションです。
2. @PostConstruct:注入が終わった直後に呼ばれる
@PostConstruct を付けたメソッドは、そのBeanに対する依存性の注入が全て完了した直後に、1回だけ 呼び出されます。
@Component
class ClassA {
@Autowired
InterfaceI i;
@PostConstruct
public void init() {
// この時点で i は注入済みであることが保証されている
System.out.println("ClassAの初期化処理を実行");
i.methodA(); // すでに利用可能
}
}
ポイントは、コンストラクタの中では保証できないことが、@PostConstruct の中では保証されている という点です。
前々回の記事で触れた通り、フィールド注入は「①空のインスタンスを作ってから、②あとからフィールドに値を差し込む」という2段階で行われます。つまり、コンストラクタが実行されている瞬間はまだ i が null の可能性がある、ということです。
「注入されたものを使った初期化処理」は、コンストラクタではなく @PostConstruct に書く。これが鉄則です。
前回の記事で推奨したコンストラクタ注入を使っている場合は、コンストラクタの中で初期化してしまっても
NullPointerExceptionにはなりません。しかし、コンストラクタは「オブジェクトの生成と値のセット」にとどめ、DB接続や外部API呼び出しのような重い業務的初期化処理は@PostConstructに切り出すのが、設計として美しいとされています。
3. @PreDestroy:破棄される直前に呼ばれる
@PreDestroy は、その逆です。アプリケーションの終了処理(コンテナのシャットダウン)が始まると、Springは管理しているBeanを破棄していきますが、そのBeanが実際にメモリ上から消える 直前 に、@PreDestroy を付けたメソッドを呼び出してくれます。
@Component
class ClassA {
@Autowired
InterfaceI i;
@PreDestroy
public void cleanup() {
// コネクションを閉じる、キャッシュを退避する、などの後片付け
System.out.println("ClassAの終了処理を実行");
}
}
これがあることで、「開いたリソースは、閉じるコードとセットで、同じクラスの中に書いておける」ようになります。呼び出し側が明示的に close() を呼び忘れる、といった事故を防ぎやすくなるわけです。
4. 補足:InitializingBean / DisposableBean というもう1つの方法
余談ですが、Springには @PostConstruct / @PreDestroy が登場する以前から使われている、インタフェースベースの方法も存在します。
| 方法 | 初期化 | 破棄 |
|---|---|---|
| アノテーション(今回紹介) | @PostConstruct |
@PreDestroy |
| インタフェース実装(旧来) | InitializingBean#afterPropertiesSet() |
DisposableBean#destroy() |
@PostConstruct はJava標準(JSR-250)のアノテーションであり、Spring独自の仕組みに縛られないというメリットがあるため、現在は基本的にこちらが使われます。「昔のコードでこんなメソッドを見かけたら、今回の話と同じ役割だ」と分かれば十分です。
⚠️ 注意:Spring Boot 3からのパッケージ変更(
javaxvsjakarta)
@PostConstructと@PreDestroyをインポートする際、使用しているSpring Bootのバージョンによって選ぶべきパッケージが異なります。
- Spring Boot 2.xまで:
javax.annotation.PostConstruct- Spring Boot 3.x以降:
jakarta.annotation.PostConstructIDE(EclipseやIntelliJなど)の自動インポートで古い
javaxの方を選んでしまうと、アノテーションを付けても一切実行されません(エラーにもなりません)。最近のSpring Bootを使っている場合は、必ずjakartaから始まる方をインポートしてください。
5. 注意点:シングルトン以外では@PreDestroyが呼ばれないことがある
前々回の記事で「Beanは基本シングルトン」という話をしましたが、これは今回のテーマにも関係します。
@PreDestroy がきちんと呼ばれるのは、シングルトンスコープのBean に限られます。@Scope("prototype") を指定したBeanは、コンテナが生成には関与しても、その後の破棄タイミングまでは管理してくれません。プロトタイプスコープのBeanで後片付けが必要な場合は、呼び出し側が自分で面倒を見る必要がある、という点は覚えておいてください。
まとめ
- Beanのライフサイクルは「生成→注入→初期化→利用→破棄前処理→破棄」という流れ
-
@PostConstructは依存性の注入が完了した直後に1回だけ呼ばれる。「注入されたものを使う初期化処理」はここに書く -
@PreDestroyはコンテナのシャットダウン時、Beanが破棄される直前に呼ばれる。リソースの後片付けに使う - 同じ役割を持つ古い方法として
InitializingBean/DisposableBeanインタフェースもあるが、現在はアノテーションが主流 -
@PreDestroyはシングルトンスコープでのみ確実に呼ばれる
「注入されたら、はいおしまい」ではなく、その前後にもきちんとフックできる場所が用意されている——これでBeanの一生を一通り追えたことになります。