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

Які етапи є у Bean lifecycle

Життєвий цикл біна у Spring — це сувора послідовність кроків, через які проходить об'єкт із моменту сканування його визначення (BeanDefinition) до знищення під час вимкнення зас...


🟢 Junior Level

Життєвий цикл біна у Spring — це сувора послідовність кроків, через які проходить об’єкт із моменту сканування його визначення (BeanDefinition) до знищення під час вимкнення застосунку.

5 основних кроків (спрощена модель)

1. Instantiation (Конструктор) → Створення об'єкта в пам'яті (Heap)
             ↓
2. Populate Properties (@Autowired) → Впровадження залежностей у поля/сетери
             ↓
3. Initialization (@PostConstruct) → Налаштування та перевірка готовності
             ↓
4. Ready for use → Обслуговування запитів та виконання бізнес-логіки
             ↓
5. Destruction (@PreDestroy) → Звільнення ресурсів при зупинці контексту

Базовий приклад із демонстрацією порядку викликів

package com.example.demo;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class NotificationManager {

    private TemplateRepository templateRepository;

    // Крок 1: Конструктор (Instantiation)
    public NotificationManager() {
        System.out.println("1. Конструктор викликано. templateRepository ще null: " 
                + (templateRepository == null));
    }

    // Крок 2: Впровадження залежностей (Populate Properties)
    @Autowired
    public void setTemplateRepository(TemplateRepository templateRepository) {
        this.templateRepository = templateRepository;
        System.out.println("2. Залежності впроваджено через setter");
    }

    // Крок 3: Ініціалізація (Initialization)
    @PostConstruct
    public void init() {
        System.out.println("3. @PostConstruct: усі залежності на місці, завантажуємо шаблони в кеш");
    }

    // Крок 4: Бізнес-метод (Ready for use)
    public void sendNotification(String message) {
        System.out.println("4. Відправлення сповіщення: " + message);
    }

    // Крок 5: Знищення (Destruction)
    @PreDestroy
    public void cleanup() {
        System.out.println("5. @PreDestroy: закриття пулу мережевих з'єднань");
    }
}

🟡 Middle Level

Усередині контейнера Spring процес розбивається на 12 канонічних кроків. Знання цієї послідовності дозволяє точно керувати створенням компонентів та уникати прихованих архітектурних дефектів.

Канонічний порядок: 12 фаз створення та утилізації біна

[ 1. Instantiation ] 
        Вибір конструктора та створення екземпляра класу в купі (heap).
             ↓
[ 2. Populate Properties ] 
        Ін'єкція залежностей у поля та сетери (виклик postProcessProperties).
             ↓
[ 3. BeanNameAware / BeanClassLoaderAware / BeanFactoryAware ] 
        Контейнер передає біну його ім'я (ID), ClassLoader та посилання на BeanFactory.
             ↓
[ 4. BPP: postProcessBeforeInitialization ] 
        Виклик зворотних викликів усіма зареєстрованими BeanPostProcessor.
        Тут CommonAnnotationBeanPostProcessor викликає методи з @PostConstruct.
        Тут ApplicationContextAwareProcessor передає ApplicationContext.
             ↓
[ 5. InitializingBean.afterPropertiesSet() ] 
        Виклик стандартного контрактного методу інтерфейсу InitializingBean.
             ↓
[ 6. Custom init-method ] 
        Виклик кастомного методу, вказаного в @Bean(initMethod = "customInit").
             ↓
[ 7. BPP: postProcessAfterInitialization ] 
        Пост-процесинг після ініціалізації.
        ТУТ створюються AOP Dynamic Proxy (CGLIB / JDK Proxy) для @Transactional, @Async.
             ↓
[ 8. READY FOR USE ] 
        Бін зареєстрований у кеші синглтонів і готовий до роботи.
             ↓
[ 9. Context Closes (Початок фази Destruction) ] 
        Контейнер ініціює коректне завершення роботи (Graceful Shutdown).
             ↓
[ 10. @PreDestroy ] 
        Виклик методів з анотацією @PreDestroy (через InitDestroyAnnotationBeanPostProcessor).
             ↓
[ 11. DisposableBean.destroy() ] 
        Виклик методу інтерфейсу DisposableBean.
             ↓
[ 12. Custom destroy-method ] 
        Виклик методу, оголошеного в @Bean(destroyMethod = "customDestroy").

Порівняння трьох механізмів ініціалізації та знищення

Spring надає три конкуруючі способи задати логіку ініціалізації та очищення. Контейнер викликає їх суворо один за одним:

Порядок Спосіб ініціалізації Спосіб знищення Стандарт / Специфікація Рекомендація
1-й @PostConstruct @PreDestroy JSR-250 / Jakarta Annotations Рекомендується (чистий код, без прив’язки до Spring API)
2-й InitializingBean.afterPropertiesSet() DisposableBean.destroy() Інтерфейси Spring Framework Застаріло (створює жорстку залежність від класів Spring)
3-й @Bean(initMethod = "...") @Bean(destroyMethod = "...") Декларативна конфігурація Ідеально для сторонніх бібліотек без власних анотацій
package com.example.demo;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;

public class LifecycleDemoBean implements InitializingBean, DisposableBean {

    // 1-ша ініціалізація
    @PostConstruct
    public void postConstruct() {
        System.out.println("Крок 1: @PostConstruct");
    }

    // 2-га ініціалізація
    @Override
    public void afterPropertiesSet() {
        System.out.println("Крок 2: InitializingBean.afterPropertiesSet()");
    }

    // 3-тя ініціалізація (вказується в @Bean(initMethod = "customInit"))
    public void customInit() {
        System.out.println("Крок 3: custom init-method");
    }

    // 1-ше знищення
    @PreDestroy
    public void preDestroy() {
        System.out.println("Крок 10: @PreDestroy");
    }

    // 2-ге знищення
    @Override
    public void destroy() {
        System.out.println("Крок 11: DisposableBean.destroy()");
    }

    // 3-тє знищення (вказується в @Bean(destroyMethod = "customDestroy"))
    public void customDestroy() {
        System.out.println("Крок 12: custom destroy-method");
    }
}

🔴 Senior Level

Внутрішній механізм Spring: AbstractAutowireCapableBeanFactory

Поглянемо на етапи крізь призму низькорівневого вихідного коду Spring (AbstractAutowireCapableBeanFactory.java):

// Спрощений псевдокод ядра Spring Framework
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
    // 1. Instantiation
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // 2. Метадані пост-процесингу (MergedBeanDefinitionPostProcessor)
    applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);

    // 3. Early Singleton Exposure (публікація фабрики для вирішення циклів)
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences 
            && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // 4. Populate Bean (впровадження полів і сетерів)
    populateBean(beanName, mbd, instanceWrapper);

    // 5. Initialize Bean (Aware, BPP, Init methods, AOP Proxy)
    Object exposedObject = initializeBean(beanName, bean, mbd);

    // 6. Реєстрація DisposableBean для синглтонів (якщо scope = singleton)
    registerDisposableBeanIfNecessary(beanName, bean, mbd);

    return exposedObject;
}

А метод initializeBean деталізує ланцюжок ініціалізації:

protected Object initializeBean(String beanName, Object bean, RootBeanDefinition mbd) {
    // Виклик Aware: BeanNameAware, BeanClassLoaderAware, BeanFactoryAware
    invokeAwareMethods(beanName, bean);

    // Виклик BPP Before (тут @PostConstruct та ApplicationContextAware)
    Object wrappedBean = applyBeanPostProcessorsBeforeInitialization(bean, beanName);

    // Виклик InitializingBean.afterPropertiesSet() та кастомного init-method
    invokeInitMethods(beanName, wrappedBean, mbd);

    // Виклик BPP After (ТУТ СТВОРЮЄТЬСЯ AOP PROXY!)
    wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);

    return wrappedBean;
}

Проблема ранньої ініціалізації BPP (BeanPostProcessor)

Один із найпідступніших дефектів у високонавантажених production-системах на Spring:

Bean 'myService' of type [com.example.MyService] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

Причина виникнення:

  1. BeanPostProcessor — це системні компоненти контейнера, які повинні ініціалізуватися раніше за всі звичайні користувацькі біни, щоб встигати обробляти їхнє створення.
  2. Якщо певний кастомний BeanPostProcessor випадково впроваджує в себе бізнес-сервіс (наприклад, UserRepository або AuditService через звичайний @Autowired):
    @Component
    public class BadCustomBpp implements BeanPostProcessor {
        @Autowired
        private AuditService auditService; // ПОМИЛКА АРХІТЕКТУРИ!
    }
    
  3. Контейнер змушений екстрено створити AuditService до того, як будуть зареєстровані решта системних BeanPostProcessor (включаючи AnnotationAwareAspectJAutoProxyCreator).
  4. Результат: AuditService створюється «голим» об’єктом. Він не отримає ні @Transactional, ні @Async, ні кешування через @Cacheable. Застосунок запуститься, але транзакції в AuditService не відкриватимуться!

Як запобігти:

  • Використовувати ліниву ін’єкцію через ObjectProvider<AuditService> або @Lazy:
     @Component
     public class GoodCustomBpp implements BeanPostProcessor {
         private final ObjectProvider<AuditService> auditServiceProvider;
    
         public GoodCustomBpp(ObjectProvider<AuditService> auditServiceProvider) {
             this.auditServiceProvider = auditServiceProvider;
         }
           
         // Звертатися до auditServiceProvider.getIfAvailable() суворо в момент виконання
     }
    

Точка створення проксі при циклічних залежностях

У стандартному сценарії AOP-проксі створюється на кроці 7 (BPP After Initialization). Проте, якщо бін втягнутий у циклічну залежність (A → B → A):

  1. Бін A інстанціюється і реєструє фабрику в singletonFactories.
  2. Бін A починає наповнюватися залежностями (populateBean) і запитує бін B.
  3. Бін B інстанціюється і запитує залежність A.
  4. Контейнер забирає ObjectFactory з 3-го кешу, яка викликає SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(beanName, mbd, bean).
  5. Увага: якщо бін A потребує проксіювання (наприклад, містить методи з @Transactional), саме на цьому ранньому етапі створюється AOP-проксі для A і поміщається у 2-й кеш earlySingletonObjects.
  6. Коли бін A нарешті доходить до кроку 7 (postProcessAfterInitialization), автопроксі-творець виявляє, що ранній проксі вже був сформований, і не обгортає бін повторно, а повертає вже наявний проксі з кешу.

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

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

Життєвий цикл біна у Spring налічує 12 ключових фаз:

  1. Створення екземпляра (Instantiation): виклик конструктора.
  2. Наповнення залежностями (Populate Properties): ін’єкція полів і сетерів.
  3. Aware-інтерфейси: BeanNameAware, BeanFactoryAware.
  4. BPP Before Initialization: виконання @PostConstruct та передача ApplicationContext.
  5. InitializingBean.afterPropertiesSet().
  6. Кастомний init-method.
  7. BPP After Initialization: створення AOP-проксі (@Transactional, @Async).
  8. Готовність (In Service).
  9. BPP Destruction / @PreDestroy.
  10. DisposableBean.destroy().
  11. Кастомний destroy-method.
  12. Для scope prototype етапи 9–11 контейнером не викликаються.

4 каверзних питання з відповідями

1. У якому порядку викликаються @PostConstruct, afterPropertiesSet() та initMethod?

Відповідь: Суворо у наступному порядку:

  1. @PostConstruct (обробляється CommonAnnotationBeanPostProcessor у фазі BPP Before).
  2. InitializingBean.afterPropertiesSet() (прямий виклик контракту інтерфейсу).
  3. initMethod (декларативний метод із @Bean(initMethod = "...")). У сучасній розробці рекомендується використовувати @PostConstruct, оскільки це стандартний механізм Java/Jakarta, що не прив’язує бізнес-код до інтерфейсів Spring Framework.

2. На якому етапі життєвого циклу створюється AOP-проксі і до яких ефектів це призводить?

Відповідь: У стандартному сценарії AOP-проксі створюється на кроці BeanPostProcessor.postProcessAfterInitialization() (через AbstractAutoProxyCreator). Це означає, що під час виконання @PostConstruct та методів Aware-інтерфейсів об’єкт є вихідним target-екземпляром (сирим об’єктом без перехоплювачів). Отже, будь-які виклики власних методів з анотаціями @Transactional, @Async чи @Cacheable з методу @PostConstruct виконуються без активації проксі-перехоплювачів!

3. Що означає попередження «Bean is not eligible for getting processed by all BeanPostProcessors»?

Відповідь: Це означає, що бін був створений передчасно, оскільки виявився залежністю для якогось BeanPostProcessor (BPP). Оскільки його було створено до повної реєстрації решти BPP, до нього не застосувалися пізніше зареєстровані пост-процесори, зокрема генератори AOP-проксі (AnnotationAwareAspectJAutoProxyCreator). Такий бін залишиться без підтримки транзакцій та інших аспектів.

4. У якому порядку викликаються методи знищення біна під час зупинки контексту?

Відповідь: Порядок суворо зворотний до ініціалізації:

  1. Метод з анотацією @PreDestroy.
  2. Метод destroy() інтерфейсу DisposableBean.
  3. Кастомний метод із декларації @Bean(destroyMethod = "...").

Червоні прапорці (чого категорично не можна говорити)

  • ❌ «AOP-проксі створюється під час роботи конструктора» — проксі створюється лише на етапі postProcessAfterInitialization (або при early singleton exposure у випадку циклів).
  • ❌ «@PreDestroy викликається для prototype-бінів» — контейнер повністю відпускає prototype-біни після етапу ініціалізації і ніколи не викликає для них методи очищення.
  • ❌ «@PostConstruct викликається до впровадження залежностей» — @PostConstruct викликається суворо ПІСЛЯ того, як усі залежності (поля/сетери) впроваджено.
  • ❌ «Можна безпечно інжектувати бізнес-сервіси баз даних у BeanPostProcessor» — це порушує послідовність створення бінів і позбавляє сервіс підтримки AOP-проксі.

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