🍃 Раздел 5 · Вопрос #9

Что делают методы с аннотацией @PostConstruct и @PreDestroy

Аннотации @PostConstruct и @PreDestroy — это стандартизированные аннотации жизненного цикла (спецификация JSR-250 / Jakarta Annotations), позволяющие определить методы, выполняе...


🟢 Junior Level

Аннотации @PostConstruct и @PreDestroy — это стандартизированные аннотации жизненного цикла (спецификация JSR-250 / Jakarta Annotations), позволяющие определить методы, выполняемые соответственно сразу после полной сборки бина и перед его уничтожением контейнером.

  • @PostConstruct — помечает метод, который автоматически вызывается контейнером ровно один раз после того, как экземпляр бина создан и все его зависимости (@Autowired, @Value) успешно внедрены.
  • @PreDestroy — помечает метод, который вызывается контейнером ровно один раз непосредственно перед уничтожением бина при штатной остановке приложения (ApplicationContext.close()).

Важно для Spring Boot 3+ (Spring Framework 6+): пакет изменился с javax.annotation.* на jakarta.annotation.*.

Базовый пример

package com.example.service;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;

@Service
public class RedisCacheService {

    private final RedisClient redisClient;

    public RedisCacheService(RedisClient redisClient) {
        this.redisClient = redisClient;
        System.out.println("1. Конструктор: объект создан");
    }

    @PostConstruct
    public void init() {
        // Выполняется после инъекции всех зависимостей
        System.out.println("2. @PostConstruct: проверка подключения к Redis и прогрев кэша");
        redisClient.connect();
        redisClient.warmUp();
    }

    public String get(String key) {
        return redisClient.get(key);
    }

    @PreDestroy
    public void cleanup() {
        // Выполняется при завершении работы приложения (graceful shutdown)
        System.out.println("3. @PreDestroy: сброс буферов на диск и закрытие сокетов");
        redisClient.flush();
        redisClient.disconnect();
    }
}

Основные сценарии применения

Аннотация Когда использовать Примеры задач
@PostConstruct Когда конструктор уже завершился и все поля внедрены Валидация обязательных параметров, прогрев локальных кэшей, регистрация в реестрах
@PreDestroy Когда контейнер завершает работу и нужно освободить ресурсы Закрытие соединений с БД, остановка кастомных пулов потоков, сброс буферов на диск

🟡 Middle Level

Правила и ограничения на сигнатуру методов

Согласно спецификации Jakarta Annotations, методы с @PostConstruct и @PreDestroy подчиняются жестким правилам:

  1. Количество аргументов: метод не должен принимать никаких параметров (сигнатура без аргументов: void methodName()).
  2. Возвращаемый тип: возвращаемое значение должно быть строго void (если метод возвращает значение, контейнер его проигнорирует).
  3. Модификаторы доступа: метод может иметь любой модификатор (public, protected, package-private, private).
  4. Модификатор static: метод не может быть static.
  5. Исключения: метод может объявлять и выбрасывать проверяемые (checked) и непроверяемые (unchecked) исключения.

Почему @PostConstruct, а не логика в конструкторе?

  1. Доступность зависимостей: при инъекции через поля (@Autowired private Repo repo;) или сеттеры внутри конструктора эти зависимости еще равны null. В @PostConstruct они гарантированно проинициализированы.
  2. Предотвращение утечки this: выполнение сложной логики или публикация ссылки на объект (this) из конструктора нарушает модель памяти Java (JMM) и может привести к гонкам потоков (thread safety issues).
  3. Разделение ответственности: конструктор отвечает только за создание объекта в памяти JVM; @PostConstruct отвечает за интеграционную инициализацию бина в экосистеме Spring.

Наследование и порядок вызовов

Если бин наследуется от базового класса, методы жизненного цикла вызываются в строгой иерархической последовательности:

package com.example.hierarchy;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public abstract class BaseService {

    @PostConstruct
    public void baseInit() {
        System.out.println("1. BaseService: @PostConstruct суперкласса");
    }

    @PreDestroy
    public void baseDestroy() {
        System.out.println("4. BaseService: @PreDestroy суперкласса");
    }
}
package com.example.hierarchy;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;

@Service
public class ChildService extends BaseService {

    @PostConstruct
    public void childInit() {
        System.out.println("2. ChildService: @PostConstruct подкласса");
    }

    @PreDestroy
    public void childDestroy() {
        System.out.println("3. ChildService: @PreDestroy подкласса");
    }
}

Порядок вызовов:

  • Инициализация: сначала вызывается @PostConstruct родительского класса (BaseService), затем @PostConstruct дочернего класса (ChildService).
  • Уничтожение: сначала вызывается @PreDestroy дочернего класса (ChildService), затем @PreDestroy базового класса (BaseService).
  • Переопределение (Override): если дочерний класс переопределяет метод родителя с той же сигнатурой, то этот метод выполнится только один раз (по правилам полиморфизма Java).

Поведение для Singleton vs Prototype

Singleton:
  → @PostConstruct вызывается один раз при старте контекста
  → @PreDestroy вызывается при context.close()

Prototype:
  → @PostConstruct вызывается при каждом запросе context.getBean()
  → @PreDestroy НЕ вызывается никогда! Spring не управляет дальнейшей судьбой prototype-бина.

🔴 Senior Level

Внутренний механизм: InitDestroyAnnotationBeanPostProcessor

Spring обрабатывает @PostConstruct и @PreDestroy не через рефлексию самого BeanFactory, а через системный BeanPostProcessor:

  • CommonAnnotationBeanPostProcessor наследует InitDestroyAnnotationBeanPostProcessor.
  • При вызове метода postProcessBeforeInitialization он ищет методы с @PostConstruct и рефлексивно вызывает их (Method.invoke()).
  • При вызове postProcessBeforeDestruction он ищет методы с @PreDestroy и вызывает их.

Ловушка: Proxy Trap в @PostConstruct

Поскольку @PostConstruct вызывается в фазе postProcessBeforeInitialization, а AOP-прокси генерируются только в фазе postProcessAfterInitialization:

  • Внутри @PostConstruct объект является сырым экземпляром (raw target).
  • Аннотации @Transactional, @Async, @Cacheable и @Secured НЕ РАБОТАЮТ при прямом вызове внутри @PostConstruct!
@Service
public class BrokenService {

    @PostConstruct
    public void init() {
        // ОШИБКА: метод выполняется без открытой транзакции!
        // Внутренний вызов this.updateDatabase() обходит прокси.
        updateDatabase(); 
    }

    @Transactional
    public void updateDatabase() {
        // Транзакция отсутствует!
    }
}

Эталонное решение: Событие готовности контекста или TransactionTemplate

package com.example.service;

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CorrectService {

    @EventListener(ApplicationReadyEvent.class)
    @Transactional
    public void onReady() {
        // БЕЗОПАСНО: контекст полностью собран, вызов идёт через AOP-прокси,
        // транзакция корректно открывается и фиксируется
        updateDatabase();
    }

    public void updateDatabase() {
        // Логика под транзакцией
    }
}

Исключения в @PostConstruct: Стратегия Fail-Fast

Если метод @PostConstruct выбрасывает любое исключение:

  1. Spring перехватывает его и заворачивает в BeanInitializationException (или BeanCreationException).
  2. Загрузка ApplicationContext немедленно прерывается с ошибкой (Fail-Fast).
  3. Приложение завершает работу с аварийным кодом возврата.

Паттерн применения: Используйте @PostConstruct для строгой проверки конфигурации (например, проверка наличия секретных ключей или доступности критической схемы данных). Если условие не выполнено — приложение должно упасть сразу, не открывая HTTP-порт для входящего трафика.

Когда @PreDestroy ГАРАНТИРОВАННО НЕ вызовется

Многие разработчики ошибочно полагают, что @PreDestroy сработает всегда. В реальности @PreDestroy не будет вызван в следующих случаях:

  1. Scope Prototype: контейнер не хранит ссылок на prototype-бины и не выполняет их деструкцию.
  2. Аварийное завершение процесса (SIGKILL): команда kill -9 в Linux или завершение процесса через Task Manager в Windows немедленно останавливает процесс ОС без отправки сигналов JVM.
  3. Вызов System.exit() без зарегистрированного Shutdown Hook: если не был вызван context.registerShutdownHook().
  4. Фатальные ошибки JVM: OutOfMemoryError или падение виртуальной машины (JVM Crash / Segfault).
  5. Зависание в другом @PreDestroy: если один из бинов заблокировал поток завершения (например, бесконечным ожиданием Thread.join()), остальные методы деструкции могут не успеть выполниться до таймаута graceful shutdown.

Механика Graceful Shutdown в Spring Boot

При получении сигнала SIGTERM (kill -15):

  1. JVM активирует зарегистрированный ShutdownHook.
  2. Spring Boot останавливает прием новых HTTP-запросов (Web Server перестает принимать трафик).
  3. Приложению дается таймаут (по умолчанию 30 секунд: spring.lifecycle.timeout-per-shutdown-phase=30s) на завершение текущих запросов.
  4. Вызываются методы SmartLifecycle.stop().
  5. Поочередно для каждого синглтон-бина вызываются методы @PreDestroy, DisposableBean.destroy() и custom destroy methods.

🎯 Шпаргалка для интервью

30-секундный ответ

@PostConstruct и @PreDestroy — это стандартизированные аннотации жизненного цикла из пакета jakarta.annotation:

  • @PostConstruct вызывается один раз после завершения конструктора и внедрения всех зависимостей, но до создания AOP-прокси. Применяется для валидации конфигурации и прогрева ресурсов.
  • @PreDestroy вызывается один раз перед уничтожением бина при закрытии контекста для освобождения ресурсов. Обе аннотации требуют методов без параметров (void methodName()). Для prototype-бинов @PreDestroy не вызывается.

4 каверзных вопроса с ответами

  1. Каковы жесткие ограничения на метод с аннотацией @PostConstruct? Ответ: Метод обязан быть без параметров (void methodName()), не может быть static, должен возвращать void и может иметь любой модификатор доступа (public, protected, package-private, private). Метод может выбрасывать исключения, что приведет к аварийной остановке старта контекста (BeanCreationException).

  2. Как работают @PostConstruct и @PreDestroy при наследовании классов? Ответ: Методы @PostConstruct выполняются сверху вниз по иерархии наследования (сначала родительский класс, затем класс-наследник). Методы @PreDestroy выполняются снизу вверх (сначала класс-наследник, затем базовый класс). Если метод переопределен (override), он выполняется один раз.

  3. Почему вызов метода с @Transactional из @PostConstruct не открывает транзакцию? Ответ: В момент вызова @PostConstruct объект является «сырым» (выполняется фаза postProcessBeforeInitialization). AOP-прокси, перехватывающий @Transactional, создается позже — в postProcessAfterInitialization. Внутри @PostConstruct вызовы идут напрямую к целевому объекту в обход прокси. Решение: использовать @EventListener(ApplicationReadyEvent.class) или программный TransactionTemplate.

  4. В каких ситуациях метод @PreDestroy не выполнится? Ответ:

    1. Для бинов со scope prototype.
    2. При жесткой остановке процесса ОС через kill -9 (SIGKILL).
    3. При фатальных ошибках JVM (OutOfMemoryError, crash JVM).
    4. Если в приложении не был зарегистрирован Shutdown Hook (context.registerShutdownHook()).

Красные флаги (чего категорически нельзя говорить)

  • ❌ «В конструкторе можно делать то же самое, что и в @PostConstruct» — нет, при field/setter injection поля в конструкторе еще равны null, плюс конструктор не должен публиковать this.
  • ❌ «@PreDestroy гарантированно выполняется всегда» — нет, при SIGKILL, crash JVM и для prototype-бинов он никогда не вызовется.
  • ❌ «Метод @PostConstruct может принимать параметры для инициализации» — ошибка спецификации JSR-250: метод обязан быть без аргументов.
  • ❌ «Вызов транзакционного метода из @PostConstruct прекрасно работает» — грубая ошибка непонимания механизмов создания AOP-прокси.

Связанные темы