🍃 Розділ 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-запитів (вбудований веб-сервер припиняє приймати трафік).
  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-проксі.

Пов’язані теми