Backend · Java

Жизненный цикл бина 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 есть и четвёртый выход — перенести логику туда, где контекст уже поднят целиком.

Уровни изоляции и распространение транзакций — соседняя тема, её спрашивают отдельно и у кандидатов любого стека.

Проверьте себя

Вопросы из разбора одним списком. Если на каждый есть ответ своими словами — тему можно считать закрытой.

  1. Опишите жизненный цикл бина в Spring
  2. Зачем нужен BeanPostProcessor?
  3. Метод помечен аннотацией, но она не сработала. Почему?
  4. JDK-прокси или CGLIB — что будет использовано?
По этой теме на платформе

Готовьтесь не вслепую

Возьмите маршрут по своей роли и отрабатывайте ответы на вопросы с AI-разбором.