DIやAutoConfigurationを使えるようになってくると、次に気になるのが「Beanってどのタイミングでどんなメソッドを呼ばせられるの?」という部分ですよね。
起動時にマスタデータをロードしたい、シャットダウン時にDB接続を閉じたい。そういった要件に答えるのがBeanのライフサイクルフックです。今回はその仕組みと4つの実装パターンを整理します。
Beanライフサイクルの全体像
Springが管理するBeanは、次の5フェーズを経て動きます。
Instantiation(インスタンス生成)
↓
依存性注入(DI)
↓
初期化コールバック(@PostConstruct など)
↓
使用中
↓
破棄コールバック(@PreDestroy など)
各フェーズで使えるフック手段はこちらです。
| フェーズ | フック手段 |
|---|---|
| 初期化 | @PostConstruct、InitializingBean#afterPropertiesSet、@Bean(initMethod) |
| 破棄 | @PreDestroy、DisposableBean#destroy、@Bean(destroyMethod) |
ポイントは、初期化コールバックは DIが完了した後 に呼ばれるということです。コンストラクタの中ではまだ注入済みフィールドを使えないので、起動時処理は @PostConstruct に書くのが基本です。
@PostConstructで初期化処理を書く
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
public class GreetingService {
private final MessageRepository messageRepository;
public GreetingService(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
@PostConstruct
public void init() {
// DIが完了した後に呼ばれる
String msg = messageRepository.findDefaultMessage();
System.out.println("初期化完了: " + msg);
}
}
インポートパスはSpring Boot 3.x以降では jakarta.annotation.PostConstruct です。2.x時代は javax.annotation.PostConstruct でしたが、Jakarta EE移行に伴って変わっています。古いサンプルコードを参考にするときは注意してください。
@PreDestroyでシャットダウン処理を書く
ApplicationContextが閉じられるとき(JVMシャットダウン時も含む)に呼ばれます。
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class ConnectionHolder {
private static final Logger log = LoggerFactory.getLogger(ConnectionHolder.class);
private SomeExternalConnection connection;
@PostConstruct
public void open() {
connection = new SomeExternalConnection();
connection.connect();
}
@PreDestroy
public void close() {
try {
connection.close();
} catch (Exception e) {
log.warn("接続のクローズに失敗しました", e);
}
}
}
なお、プロトタイプスコープのBeanでは @PreDestroy が呼ばれません。Springはプロトタイプスコープのインスタンスを生成した後に管理対象から外すため、破棄フックが動作しない仕様です。リソース解放が必要な場合は DisposableBean を実装し、呼び出し元で ((DisposableBean) bean).destroy() を明示的に呼ぶなど、手動で管理する必要があります。
InitializingBeanとDisposableBeanインターフェースを使う方法
アノテーションの代わりにインターフェースを実装する方法もあります。
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
public class LegacyService implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// @PostConstructの直後に呼ばれる
System.out.println("初期化処理");
}
@Override
public void destroy() throws Exception {
// @PreDestroyの直後に呼ばれる
System.out.println("破棄処理");
}
}
@PostConstruct と afterPropertiesSet() はどちらも初期化フェーズで呼ばれますが、SpringはまずCommonAnnotationBeanPostProcessorが @PostConstruct を処理し、その後に afterPropertiesSet() を呼び出します。実行順は @PostConstruct → afterPropertiesSet() です。破棄時も同様に @PreDestroy → destroy() の順となります。
SpringのAPIに強く依存するため、業務コードよりもSpringのライブラリやフレームワーク内部を開発する場面で使われることが多いです。
@Bean(initMethod / destroyMethod)でJava Configから指定する方法
ソースを変更できないサードパーティのクラスにフックを設定したいときに便利です。
@Configuration
public class AppConfig {
@Bean(initMethod = "start", destroyMethod = "stop")
public SomeThirdPartyService thirdPartyService() {
return new SomeThirdPartyService();
}
}
destroyMethod にはデフォルト推論という仕組みがあります。明示しない場合、Springは対象クラスに close() や shutdown() メソッドがあると自動的に破棄メソッドとして扱います。自動検出を無効にしたい場合は destroyMethod = "" と指定します。
4パターンの使い分け
| パターン | いつ使う |
|---|---|
| @PostConstruct / @PreDestroy | 通常の業務コードの第一選択。シンプルで読みやすい |
| InitializingBean / DisposableBean | Springライブラリやフレームワーク開発。Springへの依存が生じる |
| @Bean(initMethod / destroyMethod) | ソース変更不可の外部クラスにフックを追加したいとき |
迷ったら @PostConstruct / @PreDestroy を選んでおけば大丈夫です。
実務例:起動時に初期データをロードする
マスタデータをキャッシュにロードするパターンは @PostConstruct の定番ユースケースです。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class ProductCacheService {
private static final Logger log = LoggerFactory.getLogger(ProductCacheService.class);
private final ProductRepository productRepository;
private final Map<Long, Product> cache = new ConcurrentHashMap<>();
public ProductCacheService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@PostConstruct
public void loadCache() {
productRepository.findAll()
.forEach(p -> cache.put(p.getId(), p));
log.info("商品マスタをキャッシュしました: {}件", cache.size());
}
public Product findById(Long id) {
return cache.get(id);
}
}
@PostConstruct を使う利点は、このBeanが別のBeanに注入された時点でキャッシュがすでに完成している点です。一方、全Beanの初期化完了後に処理を実行したい場合は CommandLineRunner や ApplicationRunner が適しています。他のBeanの状態に依存する処理や起動完了後のウォームアップ処理など、実行タイミングを細かく制御したいケースでは CommandLineRunner のほうが扱いやすいです。
実務例:シャットダウン時にリソースを解放する
スレッドプールや外部コネクションを持つBeanでは @PreDestroy でのクリーンアップが重要です。
@Component
public class SchedulerService {
private ScheduledExecutorService scheduler;
@PostConstruct
public void start() {
scheduler = Executors.newScheduledThreadPool(4);
}
@PreDestroy
public void stop() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
組み込みTomcatはSpring Bootが自動的に閉じてくれるので、自分でTomcatをクローズする処理を書く必要はありません。
ハマりやすいポイント
@PostConstructが呼ばれない
よくある原因は、クラスがBeanとして登録されていないケースです。@Component などのアノテーションが付いているか、コンポーネントスキャン対象のパッケージに存在しているかを確認しましょう。また @PostConstruct を static メソッドや引数ありのメソッドに付けても動作しません。仕様上、対象メソッドは 戻り値void・引数なし・非static である必要があります。引数ありのメソッドに付けた場合、起動時に IllegalStateException が発生します。
複数Beanの初期化順序を制御したい
BeanAの初期化がBeanBの完了後でないと困る場合は @DependsOn("beanB") で明示的に依存関係を指定できます。
まとめ
BeanのライフサイクルはInstantiation → DI → 初期化コールバック → 使用中 → 破棄コールバックという流れです。通常の業務コードでは @PostConstruct と @PreDestroy で大半のケースはカバーできます。
Beanの生成の仕組み自体については DIとは何か や @Componentとは何か も参考にしてみてください。@Configurationとは何か や AutoConfigurationの仕組み と合わせて読むと、BeanがどのようにApplicationContextに登録されるかの全体像がつかめます。@Asyncや@Scheduledとの組み合わせは Spring Bootの非同期処理ガイド で詳しく解説しています。