🍃 Раздел 5 · Вопрос #7

Какие этапы жизненного цикла Bean

Жизненный цикл бина в Spring — это строгая последовательность шагов, через которые проходит объект с момента сканирования его определения до уничтожения при выключении приложения.


🟢 Junior Level

Жизненный цикл бина в Spring — это строгая последовательность шагов, через которые проходит объект с момента сканирования его определения до уничтожения при выключении приложения.

5 основных шагов (упрощенная модель)

1. Instantiation (Конструктор) → Создание объекта в памяти
             ↓
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-прокси.

Связанные темы