Жизненный цикл бина Spring на собеседовании
Спрашивают не список фаз, а что происходит между ними. Разбираем порядок вызовов, роль BeanPostProcessor и почему @Transactional молча не срабатывает.
Вопрос про жизненный цикл бина звучит почти на каждом собеседовании по Java, и отвечают на него обычно списком фаз: создание, внедрение зависимостей, инициализация, уничтожение. Список верный, и его почти всегда мало. Дальше идёт уточняющий, и он про то, что происходит между фазами: в какой момент бин перестаёт быть тем объектом, который вы создали, и почему из-за этого аннотация иногда не срабатывает без единой ошибки в логах. Ниже — порядок фаз с этой стороны.
Что происходит между созданием и готовностью
Путь бина от конструктора до готовности состоит из передач управления, и почти на каждой контейнер даёт постороннему коду возможность вмешаться. Порядок такой.
Создание экземпляра. Контейнер вызывает конструктор. На этом шаге бин ещё ничего не знает ни о себе, ни о контексте.
Внедрение зависимостей. Заполняются свойства и связи с другими бинами.
Aware-интерфейсы. Если бин реализует BeanNameAware, BeanFactoryAware или ApplicationContextAware, контейнер передаёт ему соответствующую часть себя. Таких интерфейсов в документации перечислен целый набор — от ApplicationEventPublisherAware до ResourceLoaderAware; принцип у всех один.
Постобработка до инициализации. Вызывается postProcessBeforeInitialization у всех зарегистрированных BeanPostProcessor.
Инициализация. Здесь работают три механизма, и они сосуществуют: аннотация @PostConstruct, метод afterPropertiesSet() интерфейса InitializingBean и произвольный метод, указанный в конфигурации. Документация описывает их как комбинируемые, и порядок между ними определён.
Постобработка после инициализации. Вызывается postProcessAfterInitialization. Вот эта фаза и есть содержательный ответ на уточняющий вопрос — о ней следующая секция.
Уничтожение. Симметрично инициализации: @PreDestroy, destroy() интерфейса DisposableBean, свой метод. Отдельно стоят Lifecycle и SmartLifecycle — они про запуск и остановку контекста, а не про отдельный бин.
Разница между двумя ответами на один вопрос — в том, названо ли назначение этих точек. «Есть семь фаз» описывает последовательность. «Между внедрением зависимостей и готовностью контейнер дважды даёт постороннему коду вмешаться в бин, и одним из вмешательств бин подменяется» описывает механизм — и из второй формулировки следует всё остальное в этом разборе.
В какой момент появляется прокси
BeanPostProcessor — интерфейс, который позволяет вмешаться в бин до и после инициализации. Формулировка «нужен для расширения возможностей контейнера» верна и почти ничего не объясняет, поэтому за ней обычно идёт уточняющий.
Содержательная часть в том, что postProcessAfterInitialization может вернуть другой объект. Именно так работает вся аннотационная магия Spring: @Transactional, @Cacheable, @Async, проверки безопасности. Обработчик берёт ваш бин, оборачивает его в прокси и отдаёт контейнеру обёртку. С этого момента все, кто получает бин по зависимости, получают прокси, а не тот объект, который создал конструктор.
Отсюда практическое следствие, которое и делает эту фазу интересной для интервьюера: пока идёт инициализация, обёртки ещё нет. Внутри @PostConstruct вы находитесь в исходном объекте, и аннотации на его методах ничего не значат — обрабатывать их некому. Инженеры, проходящие эти секции, описывают вызов транзакционного метода из @PostConstruct как типовую проверку: код выглядит корректно, транзакция не стартует, ошибки нет.
Почему @Transactional молча не срабатывает
Аннотация на методе работает не сама по себе: её обрабатывает прокси, стоящий между вызывающим кодом и вашим объектом. Если вызов до прокси не дошёл, аннотации нет.
Ровно это и происходит при самовызове. Документация Spring формулирует прямо: код снаружи держит ссылку на прокси, и вызовы через эту ссылку проходят через перехватчики; но как только управление оказалось внутри целевого объекта, любой вызов вида this.bar() идёт по ссылке this, а не через прокси — и связанный с методом advice не отрабатывает.
Из этого следуют два случая, которые выглядят как ошибка Spring, а являются следствием устройства:
- метод с
@Transactionalвызывается из соседнего метода того же класса — транзакция не открывается; - транзакционный метод вызывается из
@PostConstruct— то же самое, потому что на этой фазе прокси ещё не создан.
Диагностический признак у обоих один и тот же и стоит того, чтобы его назвать на собеседовании: никакой ошибки не будет. Код отработает, данные запишутся, отката при исключении не произойдёт. Молчание здесь и есть симптом.
Границы прокси и что с ними делают
Три ограничения прокси инженеры, проходящие эти секции, называют как то, что спрашивают вместе, — поэтому и держать их в голове стоит одним блоком. Самовызов из них только первое.
Самовызов. Разобран выше: вызов внутри объекта идёт мимо обёртки.
final-классы и методы. Если у бина нет интерфейсов, Spring делает прокси наследованием — генерирует подкласс. Унаследоваться от final-класса нельзя, переопределить final-метод тоже, поэтому перехват не состоится.
private-методы. Перехватываются вызовы, которые проходят через границу объекта; приватный метод по определению вызывается только изнутри.
Отсюда же ответ на уточняющий про тип прокси. Правило в документации простое: если целевой объект реализует хотя бы один интерфейс, используется динамический прокси JDK и проксируются все реализованные интерфейсы; если интерфейсов нет — создаётся подкласс через CGLIB. Отсюда и знакомая ошибка приведения типа: бин с интерфейсом приходит как прокси интерфейса, и привести его к конкретному классу не получится.
Что делать с самовызовом — тоже часть ожидаемого ответа. Документация перечисляет три пути и расставляет их по предпочтительности: лучший — не делать самовызов вообще, то есть вынести метод в отдельный бин; допустимый — получить ссылку на собственный прокси инъекцией и звать метод через неё; нежелательный — AopContext.currentProxy(), потому что он привязывает код к Spring AOP и требует отдельной настройки. Для случая с @PostConstruct есть и четвёртый выход — перенести логику туда, где контекст уже поднят целиком.
Уровни изоляции и распространение транзакций — соседняя тема, её спрашивают отдельно и у кандидатов любого стека.

Backend-разработчик Java
Учебный маршрут backend-разработчика на Java: язык и JVM, коллекции и многопоточность, Spring, реляционные данные и JPA, Kafka, безопасность, продакшн-инженерия и архитектура.

Backend Developer Java
Глубокое понимание Java и JVM для backend-разработки. Курс разбирает не синтаксис, а семантику языка и поведение платформы: коллекции и их стоимость, Stream API, устройство JVM — память, сборку мусора, JIT, — модель памяти и многопоточность, внутренности Spring (контейнер, прокси, транзакции), JPA/Hibernate, Kafka и production-практики: тестирование, наблюдаемость, контейнеры, устойчивость. Цель — научиться видеть стоимость абстракций и поведение кода под нагрузкой, а не просто заставлять его работать.
Готовьтесь не вслепую
Возьмите маршрут по своей роли и отрабатывайте ответы на вопросы с AI-разбором.