🍃 Розділ 5 · Питання #6

Що таке Bean Lifecycle

4. Діагностика циклічних залежностей: розуміти, чому цикли можуть вирішуватися при setter-ін'єкції, але аварійно завершують підняття контексту при ін'єкції через конструктор.


🟢 Junior Level

Bean Lifecycle (життєвий цикл біна) — це послідовність етапів, через які проходить об’єкт під управлінням Spring IoC-контейнера: від зчитування метаданих (BeanDefinition), виділення пам’яті в купі (heap) та впровадження залежностей до фази ініціалізації, активної експлуатації та коректного звільнення ресурсів під час зупинки застосунку.

Навіщо розробнику знати Lifecycle біна

  1. Ініціалізація ресурсів у правильний момент: відкривати з’єднання, прогрівати кеші та валідувати конфігурацію потрібно суворо тоді, коли всі залежності вже впроваджені, але бін ще не почав обробляти вхідний трафік.
  2. Розуміння роботи проксі та магії Spring: усвідомлювати, чому анотації на кшталт @Transactional, @Async чи @Secured не спрацьовують під час виклику з методу @PostConstruct.
  3. Безпечне звільнення ресурсів: запобігати витокам пам’яті та дескрипторів, коректно зупиняючи фонові пули потоків, мережеві з’єднання та файлові канали під час закриття контексту.
  4. Діагностика циклічних залежностей: розуміти, чому цикли можуть вирішуватися при 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):

  1. createBeanInstance(beanName, mbd, args):
    • Вибір відповідного конструктора (Constructor Resolution).
    • Інстанціювання через рефлексію (Constructor.newInstance()) або CGLIB. На цьому етапі об’єкт уже фізично існує в купі (Heap), але його поля (крім параметрів конструктора) ще містять null.
  2. applyMergedBeanDefinitionPostProcessors:
    • Пост-процесори сканують клас біна на наявність анотацій @Autowired, @Value, @Resource, @PostConstruct, кешуючи метадані в RootBeanDefinition.
  3. Early Singleton Exposure (Рання публікація):
    • Якщо бін є синглтоном, дозволені циклічні посилання і бін перебуває у процесі створення (isSingletonCurrentlyInCreation(beanName)), фабрика обгортає ще не ініціалізований об’єкт в ObjectFactory і розміщує в 3-му рівні кешу:
      addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
      
  4. populateBean(beanName, mbd, instanceWrapper):
    • Виклик InstantiationAwareBeanPostProcessor.postProcessProperties().
    • Впровадження залежностей через сетери та рефлексивне заповнення приватних полів (AutowiredAnnotationBeanPostProcessor).
  5. initializeBean(beanName, exposedObject, mbd):
    • Виклик Aware-методів (BeanNameAware, BeanFactoryAware).
    • applyBeanPostProcessorsBeforeInitialization: виконання @PostConstruct (через InitDestroyAnnotationBeanPostProcessor / CommonAnnotationBeanPostProcessor).
    • invokeInitMethods: виклик afterPropertiesSet() з InitializingBean, а потім кастомного init-method.
    • applyBeanPostProcessorsAfterInitialization: обгортання в динамічний AOP-проксі (AbstractAutoProxyCreator), якщо бін підпадає під Pointcut транзакцій, безпеки або кастомних аспектів.

Трирівневий кеш синглтонів (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) після завершення старту:

  1. При ін’єкції через конструктор поля позначаються як final. Специфікація JMM (§17.5) гарантує Freeze Action: будь-який потік гарантовано бачить проініціалізовані значення final-полів без додаткової синхронізації.
  2. При використанні setter/field injection поля не є final. Проте Spring гарантує бар’єр видимості (Safe Publication): додавання готового синглтона в карту singletonObjects виконується всередині блоку synchronized (this.singletonObjects), а читання singletonObjects у конкурентному середовищі відбувається з ConcurrentHashMap, що встановлює відношення happens-before між потоком ініціалізації контексту та робочими потоками сервлетів.

🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

Bean Lifecycle — це сувора послідовність етапів створення, конфігурування та утилізації біна під контролем IoC-контейнера. Він складається з п’яти макро-фаз:

  1. Зчитування BeanDefinition та модифікація метаданих через BeanFactoryPostProcessor.
  2. Інстанціювання (виклик конструктора).
  3. Впровадження залежностей (populateBean, сетери/поля).
  4. Ініціалізація (Aware-інтерфейси → BPP Before / @PostConstruct → InitializingBean → Custom init-method → BPP After / обгортання в AOP-проксі).
  5. Знищення (@PreDestroy → DisposableBean → custom destroy-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 керує фазами старту/зупинки компонентів застосунку в правильному глобальному порядку.

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