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("破棄処理");
    }
}

@PostConstructafterPropertiesSet() はどちらも初期化フェーズで呼ばれますが、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 / DisposableBeanSpringライブラリやフレームワーク開発。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の初期化完了後に処理を実行したい場合は CommandLineRunnerApplicationRunner が適しています。他の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 などのアノテーションが付いているか、コンポーネントスキャン対象のパッケージに存在しているかを確認しましょう。また @PostConstructstatic メソッドや引数ありのメソッドに付けても動作しません。仕様上、対象メソッドは 戻り値void・引数なし・非static である必要があります。引数ありのメソッドに付けた場合、起動時に IllegalStateException が発生します。

複数Beanの初期化順序を制御したい

BeanAの初期化がBeanBの完了後でないと困る場合は @DependsOn("beanB") で明示的に依存関係を指定できます。

まとめ

BeanのライフサイクルはInstantiation → DI → 初期化コールバック → 使用中 → 破棄コールバックという流れです。通常の業務コードでは @PostConstruct@PreDestroy で大半のケースはカバーできます。

Beanの生成の仕組み自体については DIとは何か@Componentとは何か も参考にしてみてください。@Configurationとは何かAutoConfigurationの仕組み と合わせて読むと、BeanがどのようにApplicationContextに登録されるかの全体像がつかめます。@Asyncや@Scheduledとの組み合わせは Spring Bootの非同期処理ガイド で詳しく解説しています。