Що таке Bean Lifecycle
4. Діагностика циклічних залежностей: розуміти, чому цикли можуть вирішуватися при setter-ін'єкції, але аварійно завершують підняття контексту при ін'єкції через конструктор.
🟢 Junior Level
Bean Lifecycle (життєвий цикл біна) — це послідовність етапів, через які проходить об’єкт під управлінням Spring IoC-контейнера: від зчитування метаданих (BeanDefinition), виділення пам’яті в купі (heap) та впровадження залежностей до фази ініціалізації, активної експлуатації та коректного звільнення ресурсів під час зупинки застосунку.
Навіщо розробнику знати Lifecycle біна
- Ініціалізація ресурсів у правильний момент: відкривати з’єднання, прогрівати кеші та валідувати конфігурацію потрібно суворо тоді, коли всі залежності вже впроваджені, але бін ще не почав обробляти вхідний трафік.
- Розуміння роботи проксі та магії Spring: усвідомлювати, чому анотації на кшталт
@Transactional,@Asyncчи@Securedне спрацьовують під час виклику з методу@PostConstruct. - Безпечне звільнення ресурсів: запобігати витокам пам’яті та дескрипторів, коректно зупиняючи фонові пули потоків, мережеві з’єднання та файлові канали під час закриття контексту.
- Діагностика циклічних залежностей: розуміти, чому цикли можуть вирішуватися при setter-ін’єкції, але аварійно завершують підняття контексту при ін’єкції через конструктор.
Ключові макро-етапи
1. Instantiation (Створення об'єкта в пам'яті через конструктор)
↓
2. Populate Properties (Впровадження залежностей @Autowired / setter)
↓
3. Initialization (Виклик @PostConstruct / afterPropertiesSet / init-method)
↓
4. In Service / Ready (Бін готовий та обслуговує запити)
↓
5. Destruction (Виклик @PreDestroy / DisposableBean.destroy при зупинці контексту)
Важливо для Spring Boot 3+ (Spring Framework 6+): анотації життєвого циклу мігрували з пакета
javax.annotation.*уjakarta.annotation.*.
Базовий приклад у Spring Boot 3
package com.example.service;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
@Service
public class OrderProcessingService {
private final PaymentClient paymentClient;
// 1. Instantiation: контейнер викликає конструктор та передає PaymentClient
public OrderProcessingService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
System.out.println("1. Instantiation: об'єкт створено, залежності через конструктор впроваджено");
}
// 3. Initialization: викликається ПІСЛЯ того, як усі залежності впроваджено
@PostConstruct
public void init() {
System.out.println("3. Initialization: валідація конфігурації, прогрів клієнтів");
paymentClient.ping();
}
// 4. In Service: бізнес-методи біна
public void processOrder(Long orderId) {
paymentClient.charge(orderId);
}
// 5. Destruction: викликається перед вивантаженням біна при закритті ApplicationContext
@PreDestroy
public void cleanup() {
System.out.println("5. Destruction: коректне закриття мережевих з'єднань");
}
}
🟡 Middle Level
Детальна архітектура фаз контейнера
Створенням та супроводом бінів керує реалізація інтерфейсу AutowireCapableBeanFactory (зазвичай AbstractAutowireCapableBeanFactory). Процес розбитий на чіткі внутрішні фази:
[ Зчитування BeanDefinition ] (XML, @Component, @Bean)
↓
[ BeanFactoryPostProcessor ] (Модифікація метаданих до створення бінів)
↓
[ Instantiation ] (Вибір конструктора, створення сирого об'єкта в купі)
↓
[ Populate Properties ] (Впровадження полів і сетерів через BPP)
↓
[ Aware Interfaces ] (BeanNameAware, BeanFactoryAware, ApplicationContextAware)
↓
[ BPP: postProcessBeforeInitialization ] (@PostConstruct через CommonAnnotationBeanPostProcessor)
↓
[ InitializingBean: afterPropertiesSet() ]
↓
[ Custom init-method ] (@Bean(initMethod = "customInit"))
↓
[ BPP: postProcessAfterInitialization ] (Створення AOP Dynamic Proxy / CGLIB)
↓
[ READY TO USE ]
↓
[ Destruction Phase ] (@PreDestroy → DisposableBean.destroy() → destroy-method)
Усвідомлення Aware-інтерфейсів
Якщо біну потрібен прямий доступ до системних компонентів контейнера, він реалізує відповідні маркерні Aware-інтерфейси. Контейнер впроваджує їх у суворому порядку:
BeanNameAware— передає строковий ID / ім’я біна в контексті.BeanClassLoaderAware— завантажувач класів, використаний контейнером.BeanFactoryAware— посилання на низькорівневийBeanFactory.EnvironmentAware— активні профілі та змінні оточення.ApplicationContextAware— високорівневий контекст застосунку (реалізується через спеціальнийApplicationContextAwareProcessor, що єBeanPostProcessor).
Singleton vs Prototype у життєвому циклі
Поведінка контейнера кардинально відрізняється залежно від scope біна:
| Критерій | Singleton (за замовчуванням) |
Prototype |
|---|---|---|
| Створення екземпляра | Один раз під час старту контексту (якщо не @Lazy) |
Щоразу при виклику getBean() або ін’єкції |
| Впровадження залежностей | Один раз під час ініціалізації | Щоразу для нового екземпляра |
Фаза @PostConstruct |
Виконується один раз контейнером | Виконується для кожного створеного екземпляра |
| Збереження посилання | У кеші синглтонів контейнера | Не утримується контейнером після видачі |
Фаза @PreDestroy |
Виконується при context.close() |
❌ НЕ викликається контейнером! |
Критичне правило Prototype: контейнер повністю «забуває» про prototype-бін одразу після завершення фази ініціалізації та передачі клієнту. Контейнер не відстежує його життєвий цикл і не викликає
@PreDestroy/DisposableBean.destroy(). Очищення ресурсів prototype-біна (закриття файлів, з’єднань, пулів) зобов’язаний здійснювати клієнтський код або спеціальний кастомнийBeanPostProcessor!
// Коректне ручне звільнення ресурсів для Prototype:
PrototypeWorker worker = applicationContext.getBean(PrototypeWorker.class);
try {
worker.executeTask();
} finally {
// Звільнення ресурсів повністю лежить на стороні клієнта
worker.cleanup();
}
Контроль порядку старту та зупинки: SmartLifecycle
Коли застосунку необхідно запускати або зупиняти цілі підсистеми (споживачі Kafka, слухачі RabbitMQ, вбудований Netty/Tomcat сервер) у строго визначеному порядку після повного збирання контексту, використовують інтерфейс SmartLifecycle:
package com.example.lifecycle;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
@Component
public class QueueConsumerManager implements SmartLifecycle {
private volatile boolean running = false;
@Override
public void start() {
// Запуск фонових підсистем ПІСЛЯ того, як усі біни та проксі ініціалізовані
this.running = true;
System.out.println("SmartLifecycle: черга запущена");
}
@Override
public void stop() {
this.running = false;
System.out.println("SmartLifecycle: черга зупинена");
}
@Override
public boolean isRunning() {
return running;
}
@Override
public int getPhase() {
// Менше число = раніше запускається, пізніше зупиняється
return 100;
}
@Override
public boolean isAutoStartup() {
return true; // Автоматичний запуск разом зі стартом контейнера
}
}
🔴 Senior Level
Нутрощі AbstractAutowireCapableBeanFactory: метод doCreateBean
У вихідному коді Spring створення біна зосереджено в методі doCreateBean(beanName, mbd, args):
createBeanInstance(beanName, mbd, args):- Вибір відповідного конструктора (Constructor Resolution).
- Інстанціювання через рефлексію (
Constructor.newInstance()) або CGLIB. На цьому етапі об’єкт уже фізично існує в купі (Heap), але його поля (крім параметрів конструктора) ще містятьnull.
applyMergedBeanDefinitionPostProcessors:- Пост-процесори сканують клас біна на наявність анотацій
@Autowired,@Value,@Resource,@PostConstruct, кешуючи метадані вRootBeanDefinition.
- Пост-процесори сканують клас біна на наявність анотацій
- Early Singleton Exposure (Рання публікація):
- Якщо бін є синглтоном, дозволені циклічні посилання і бін перебуває у процесі створення (
isSingletonCurrentlyInCreation(beanName)), фабрика обгортає ще не ініціалізований об’єкт вObjectFactoryі розміщує в 3-му рівні кешу:addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
- Якщо бін є синглтоном, дозволені циклічні посилання і бін перебуває у процесі створення (
populateBean(beanName, mbd, instanceWrapper):- Виклик
InstantiationAwareBeanPostProcessor.postProcessProperties(). - Впровадження залежностей через сетери та рефлексивне заповнення приватних полів (
AutowiredAnnotationBeanPostProcessor).
- Виклик
initializeBean(beanName, exposedObject, mbd):- Виклик Aware-методів (
BeanNameAware,BeanFactoryAware). applyBeanPostProcessorsBeforeInitialization: виконання@PostConstruct(черезInitDestroyAnnotationBeanPostProcessor/CommonAnnotationBeanPostProcessor).invokeInitMethods: викликafterPropertiesSet()зInitializingBean, а потім кастомногоinit-method.applyBeanPostProcessorsAfterInitialization: обгортання в динамічний AOP-проксі (AbstractAutoProxyCreator), якщо бін підпадає під Pointcut транзакцій, безпеки або кастомних аспектів.
- Виклик Aware-методів (
Трирівневий кеш синглтонів (DefaultSingletonBeanRegistry)
Для безконфліктного розв’язання циклічних залежностей типу «A залежить від B, а B залежить від A» (при Setter або Field Injection) Spring використовує три Map-структури:
DefaultSingletonBeanRegistry:
1. singletonObjects (Map<String, Object>):
Готові, повністю ініціалізовані та проксійовані синглтони.
2. earlySingletonObjects (Map<String, Object>):
«Ранні» посилання на біни. Об'єкт уже створено в купі, але він ще не пройшов фазу ініціалізації.
Використовується для запобігання повторному створенню проксі при множинних циклах.
3. singletonFactories (Map<String, ObjectFactory<?>>):
Фабрики ранніх посилань. Лямбда викликає SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference().
Потік розв'язання залежності:
getSingleton(beanName, true):
1. Шукаємо в singletonObjects. Знайшли? Повертаємо.
2. Не знайшли? Перевіряємо: чи бін у статусі створення (isSingletonCurrentlyInCreation)?
3. Шукаємо в earlySingletonObjects. Знайшли? Повертаємо.
4. Не знайшли? Синхронізуємося за lock-монітором singletonObjects.
5. Беремо ObjectFactory з singletonFactories, викликаємо factory.getObject().
6. Кладемо результат в earlySingletonObjects, видаляємо з singletonFactories.
Чому Constructor Injection не розв’язується 3-рівневим кешем: Щоб зареєструвати фабрику в
singletonFactories, об’єкт спочатку має бути інстанційований у купі. При ін’єкції через конструктор потік виконання (Thread) застрягає на кроці1. createBeanInstance, намагаючись отримати залежність B, для створення якої потрібен A, який фізично ще не інстанційовано. Виникає винятокBeanCurrentlyInCreationException.
Проблема: @PostConstruct та AOP Proxy
Найпоширеніша архітектурна пастка: виклик методу з @Transactional або @Async із методу @PostConstruct.
@Service
public class CurrencyRateService {
private final CurrencyClient client;
public CurrencyRateService(CurrencyClient client) {
this.client = client;
}
@PostConstruct
public void init() {
// ПОМИЛКА: транзакція НЕ відкриється!
// У момент виклику @PostConstruct об'єкт є «сирим» (target object).
// Проксі створюється лише на наступному етапі: postProcessAfterInitialization!
refreshRatesInTransaction();
}
@Transactional
public void refreshRatesInTransaction() {
// Запис у базу без відкритої транзакції!
}
}
Хронологія виконання:
1. Constructor -> Створено сирий об'єкт Target
2. BPP: BeforeInitialization -> Викликано @PostConstruct (this = Target, проксі ще немає!)
3. InitializingBean -> afterPropertiesSet()
4. BPP: AfterInitialization -> Створено Proxy, який обгортає Target
Еталонне рішення: Слухач події готовності контексту
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 CurrencyRateService {
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void onApplicationReady() {
// Безпечно: контекст повністю стартував, усі BPP відпрацювали,
// виклик перехоплюється згенерованим проксі-об'єктом
refreshRates();
}
public void refreshRates() {
// Коректно виконується в межах активної транзакції
}
}
JMM (Java Memory Model) та безпечна публікація бінів
Spring гарантує потокобезпеку синглтон-бінів під час їх читання кількома потоками (Worker Threads) після завершення старту:
- При ін’єкції через конструктор поля позначаються як
final. Специфікація JMM (§17.5) гарантує Freeze Action: будь-який потік гарантовано бачить проініціалізовані значенняfinal-полів без додаткової синхронізації. - При використанні setter/field injection поля не є
final. Проте Spring гарантує бар’єр видимості (Safe Publication): додавання готового синглтона в картуsingletonObjectsвиконується всередині блокуsynchronized (this.singletonObjects), а читанняsingletonObjectsу конкурентному середовищі відбувається зConcurrentHashMap, що встановлює відношення happens-before між потоком ініціалізації контексту та робочими потоками сервлетів.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
Bean Lifecycle — це сувора послідовність етапів створення, конфігурування та утилізації біна під контролем IoC-контейнера. Він складається з п’яти макро-фаз:
- Зчитування
BeanDefinitionта модифікація метаданих черезBeanFactoryPostProcessor.- Інстанціювання (виклик конструктора).
- Впровадження залежностей (
populateBean, сетери/поля).- Ініціалізація (Aware-інтерфейси → BPP Before /
@PostConstruct→InitializingBean→ Custominit-method→ BPP After / обгортання в AOP-проксі).- Знищення (
@PreDestroy→DisposableBean→ customdestroy-method). Дляprototype-бінів фаза знищення контейнером не викликається.
4 каверзних питання з відповідями
1. Чому анотації @Transactional та @Async не спрацьовують всередині @PostConstruct?
Відповідь: У момент виконання методу @PostConstruct об’єкт ще не пройшов етап postProcessAfterInitialization, на якому створюється AOP-проксі (AbstractAutoProxyCreator). Усередині @PostConstruct ключове слово this посилається на «сирий» екземпляр класу без перехоплювачів. Рішення: перенести логіку в слухач події @EventListener(ApplicationReadyEvent.class) або @EventListener(ContextRefreshedEvent.class).
2. Як контейнер розрулює циклічні залежності і коли він безсилий?
Відповідь: Через 3-рівневий кеш синглтонів у DefaultSingletonBeanRegistry. Фабрика раннього посилання (singletonFactories) публікує ще не проініціалізований об’єкт у earlySingletonObjects. Це працює для setter/field injection. Для constructor injection це не працює, оскільки об’єкт фізично неможливо створити в купі без параметрів конструктора — Spring викине виняток BeanCurrentlyInCreationException.
3. Що відбувається з prototype-біном під час закриття ApplicationContext?
Відповідь: Нічого. Spring віддає посилання на створений prototype-екземпляр клієнту і не зберігає посилання на нього. Фаза @PreDestroy та виклики DisposableBean для prototype-бінів ніколи не виконуються контейнером. Відповідальність за очищення пам’яті та закриття системних дескрипторів повністю покладена на клієнтський код.
4. У чому різниця між BeanFactoryPostProcessor (BFPP) та BeanPostProcessor (BPP)?
Відповідь: BFPP працює з метаданими (BeanDefinition) до того, як будуть створені самі об’єкти класів (наприклад, підстановка властивостей з application.properties через PropertySourcesPlaceholderConfigurer). BPP працює з екземплярами об’єктів у пам’яті (інтерцептори до та після ініціалізації, впровадження @Autowired, створення AOP-проксі).
Червоні прапорці (чого категорично не можна говорити)
- ❌ «У конструкторі всі
@Autowiredполя вже впроваджені» — неправда, якщо тільки ін’єкція не виконується безпосередньо через сам конструктор. - ❌ «Spring викликає
@PreDestroyдля всіх типів бінів, включаючи Prototype» — груба помилка. Контейнер не відстежує деструкцію Prototype. - ❌ «
@PostConstructта створення проксі відбуваються одночасно» — ні, проксі створюється суворо після виклику@PostConstruct. - ❌ «
SmartLifecycleта@PostConstructроблять одне й те саме» —@PostConstructініціалізує внутрішній стан конкретного біна;SmartLifecycleкерує фазами старту/зупинки компонентів застосунку в правильному глобальному порядку.