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

Что такое Bean Lifecycle

4. Диагностика циклических зависимостей: понимать, почему циклы разрешаются при сеттерном внедрении, но валят контекст при внедрении через конструктор.


🟢 Junior Level

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

Зачем разработчику знать Lifecycle бина

  1. Инициализация ресурсов в правильный момент: открывать соединения, прогревать кэши и валидировать конфигурацию нужно строго тогда, когда все зависимости уже внедрены, но бин еще не начал обслуживать входящий трафик.
  2. Понимание работы прокси и магии Spring: осознавать, почему аннотации @Transactional, @Async или @Secured не срабатывают при вызове из @PostConstruct.
  3. Безопасное освобождение ресурсов: предотвращать утечки памяти, корректно останавливая фоновые пулы потоков, сетевые соединения и файловые дескрипторы при остановке контекста.
  4. Диагностика циклических зависимостей: понимать, почему циклы разрешаются при сеттерном внедрении, но валят контекст при внедрении через конструктор.

Ключевые макро-этапы

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 ] (Выбор конструктора, создание сырого объекта в heap)
             ↓
[ 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-сервер) в строго определенном порядке после полной сборки контекста, используют интерфейс 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 использует 3 мапы:

DefaultSingletonBeanRegistry:
1. singletonObjects (Map<String, Object>):
   Готовые, полностью инициализированные и проксированные синглтоны.
   
2. earlySingletonObjects (Map<String, Object>):
   «Ранние» ссылки на бины. Объект уже создан в куче, но еще не прошел инициализацию.
   Используется для предотвращения повторного создания прокси при множественных циклах.
   
3. singletonFactories (Map<String, ObjectFactory<?>>):
   Фабрики ранних ссылок. Лямбда вызывает SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference().
Поток разрешения зависимости:
getSingleton(beanName, true):
  1. Ищем в singletonObjects. Нашли? Возвращаем.
  2. Не нашли? Проверяем: бин в статусе создания?
  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 управляет фазами старта/остановки компонентов приложения в правильном глобальном порядке.

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