Що роблять методи з анотаціями @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 підпорядковуються суворим правилам:
- Кількість аргументів: метод не повинен приймати жодних параметрів (сигнатура без аргументів:
void methodName()). - Тип повернення: тип значення, що повертається, має бути суворо
void(якщо метод повертає значення, контейнер його проігнорує). - Модифікатори доступу: метод може мати будь-який модифікатор (
public,protected,package-private,private). - Модифікатор
static: метод не може бутиstatic. - Винятки: метод може оголошувати та викидати як перевіряні (
checked), так і неперевіряні (unchecked) винятки.
Чому саме @PostConstruct, а не логіка в конструкторі?
- Доступність залежностей: при ін’єкції через поля (
@Autowired private Repo repo;) або сетери всередині конструктора ці залежності ще дорівнюютьnull. У@PostConstructвони гарантовано проініціалізовані. - Запобігання витоку
this: виконання складної логіки або публікація посилання на об’єкт (this) із конструктора порушує модель пам’яті Java (JMM) і може призвести до станів гонитви (thread safety issues). - Розділення відповідальності: конструктор відповідає лише за створення об’єкта в пам’яті 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 викидає будь-який виняток:
- Spring перехоплює його та загортає у
BeanInitializationException(абоBeanCreationException). - Завантаження
ApplicationContextнегайно переривається з помилкою (Fail-Fast). - Застосунок завершує роботу з аварійним кодом повернення.
Патерн застосування: Використовуйте
@PostConstructдля суворої перевірки конфігурації (наприклад, перевірка наявності секретних ключів або доступності критичної схеми даних). Якщо умова не виконана — застосунок повинен впасти негайно, не відкриваючи HTTP-порт для вхідного трафіку.
Коли @PreDestroy ГАРАНТОВАНО НЕ викличеться
Багато розробників помилково вважають, що @PreDestroy спрацює завжди. У реальності @PreDestroy не буде викликаний у наступних випадках:
- Scope Prototype: контейнер не зберігає посилань на prototype-біни і не здійснює їхню деструкцію.
- Аварійне завершення процесу (SIGKILL): команда
kill -9у Linux або завершення процесу черезTask Managerу Windows негайно зупиняє процес ОС без надсилання сигналів у JVM. - Виклик
System.exit()без зареєстрованого Shutdown Hook: якщо не було викликаноcontext.registerShutdownHook(). - Фатальні помилки JVM:
OutOfMemoryErrorабо падіння віртуальної машини (JVM Crash / Segfault). - Зависання в іншому
@PreDestroy: якщо один із бінів заблокував потік завершення (наприклад, нескінченним очікуваннямThread.join()), інші методи деструкції можуть не встигнути виконатися до таймауту graceful shutdown.
Механіка Graceful Shutdown у Spring Boot
При отриманні сигналу SIGTERM (kill -15):
- JVM активує зареєстрований
ShutdownHook. - Spring Boot зупиняє прийом нових HTTP-запитів (вбудований веб-сервер припиняє приймати трафік).
- Застосунку надається таймаут (за замовчуванням 30 секунд:
spring.lifecycle.timeout-per-shutdown-phase=30s) на завершення поточних запитів. - Викликаються методи
SmartLifecycle.stop(). - Почергово для кожного синглтон-біна викликаються методи
@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 не виконається?
Відповідь:
- Для бінів зі scope
prototype. - При аварійній зупинці процесу ОС через
kill -9(SIGKILL). - При фатальних помилках JVM (
OutOfMemoryError, crash JVM). - Якщо в застосунку не було зареєстровано
Shutdown Hook(context.registerShutdownHook()).
Червоні прапорці (чого категорично не можна говорити)
- ❌ «У конструкторі можна робити те саме, що й у
@PostConstruct» — ні, при field/setter injection поля в конструкторі ще дорівнюютьnull, до того ж конструктор не повинен публікувати посиланняthis. - ❌ «
@PreDestroyгарантовано виконується завжди» — ні, приSIGKILL, crash JVM та для prototype-бінів він ніколи не викличеться. - ❌ «Метод
@PostConstructможе приймати параметри для ініціалізації» — помилка специфікації JSR-250: метод зобов’язаний бути без аргументів. - ❌ «Виклик транзакційного методу з
@PostConstructчудово працює» — груба помилка нерозуміння механізмів створення AOP-проксі.