Что такое Bean Lifecycle
4. Диагностика циклических зависимостей: понимать, почему циклы разрешаются при сеттерном внедрении, но валят контекст при внедрении через конструктор.
🟢 Junior Level
Bean Lifecycle (жизненный цикл бина) — это последовательность этапов, через которые проходит объект под управлением Spring IoC-контейнера: от считывания метаданных (BeanDefinition), выделения памяти в куче (heap) и внедрения зависимостей до фазы инициализации, активной работы и корректного освобождения ресурсов при завершении работы приложения.
Зачем разработчику знать Lifecycle бина
- Инициализация ресурсов в правильный момент: открывать соединения, прогревать кэши и валидировать конфигурацию нужно строго тогда, когда все зависимости уже внедрены, но бин еще не начал обслуживать входящий трафик.
- Понимание работы прокси и магии Spring: осознавать, почему аннотации
@Transactional,@Asyncили@Securedне срабатывают при вызове из@PostConstruct. - Безопасное освобождение ресурсов: предотвращать утечки памяти, корректно останавливая фоновые пулы потоков, сетевые соединения и файловые дескрипторы при остановке контекста.
- Диагностика циклических зависимостей: понимать, почему циклы разрешаются при сеттерном внедрении, но валят контекст при внедрении через конструктор.
Ключевые макро-этапы
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):
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 использует 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) после старта:
- При инъекции через конструктор поля помечаются
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 каверзных вопроса с ответами
-
Почему аннотации
@Transactionalи@Asyncне срабатывают внутри@PostConstruct? Ответ: В момент выполнения@PostConstructобъект еще не прошел этапpostProcessAfterInitialization, на котором создается AOP-прокси (AbstractAutoProxyCreator). Внутри@PostConstructключевое словоthisссылается на «сырой» экземпляр класса. Решение: перенести логику в слушатель события@EventListener(ApplicationReadyEvent.class)или@EventListener(ContextRefreshedEvent.class). -
Как контейнер разруливает циклические зависимости и когда он бессилен? Ответ: Через 3-уровневый кэш синглтонов в
DefaultSingletonBeanRegistry. Фабрика ранней ссылки (singletonFactories) публикует еще не проинициализированный объект вearlySingletonObjects. Это работает для setter/field injection. Для constructor injection это не работает, так как объект физически нельзя создать в куче без параметров конструктора — Spring выброситBeanCurrentlyInCreationException. -
Что происходит с
prototype-бином при закрытииApplicationContext? Ответ: Ничего. Spring отдает ссылку на созданный prototype-бин клиенту и удаляет внутреннюю связь с ним. Фаза@PreDestroyи вызовыDisposableBeanдля prototype-бинов никогда не выполняются контейнером. Ответственность за очистку памяти и закрытие дескрипторов полностью возложена на клиентский код. -
В чём разница между
BeanFactoryPostProcessor(BFPP) иBeanPostProcessor(BPP)? Ответ:BFPPработает с метаданными (BeanDefinition) до того, как будут созданы объекты классов (например, подстановка свойств изapplication.propertiesчерезPropertySourcesPlaceholderConfigurer).BPPработает с экземплярами объектов в памяти (интерцепторы до и после инициализации, внедрение@Autowired, создание AOP-прокси).
Красные флаги (чего категорически нельзя говорить)
- ❌ «В конструкторе все
@Autowiredполя уже внедрены» — ложь, если только инъекция не идет напрямую через сам конструктор. - ❌ «Spring вызывает
@PreDestroyдля всех типов бинов, включая Prototype» — грубая ошибка. Контейнер не отслеживает деструкцию Prototype. - ❌ «
@PostConstructи создание прокси происходят одновременно» — нет, прокси создается строго после вызова@PostConstruct. - ❌ «
SmartLifecycleи@PostConstructделают одно и то же» —@PostConstructинициализирует внутреннее состояние конкретного бина;SmartLifecycleуправляет фазами старта/остановки компонентов приложения в правильном глобальном порядке.