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

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

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

Сначала проговорите ответ, потом сверьтесь с текстом.

Опишите жизненный цикл бина в Spring

Что проверяют: фазы — не этапы сборки объекта, а точки, где контейнер передаёт управление наружу.

Частая ошибка: назвать список и остановиться. Уточняющий вопрос уходит к тому, что на этих точках происходит.

Раздел «Что происходит между созданием и готовностью»
Зачем нужен BeanPostProcessor?

Что проверяют: знаете ли вы, что постобработчик может подменить объект целиком. Без этого не объясняется, откуда вообще берутся прокси.

Частая ошибка: ответить «для расширения функциональности». Формулировка правильная и пустая: она подходит к любому интерфейсу расширения и не показывает, что вы понимаете механизм.

Раздел «В какой момент появляется прокси»
Метод помечен аннотацией, но она не сработала. Почему?

Что проверяют: отсутствие ошибки здесь само служит диагностическим признаком. Рассуждение начинается с вопроса о пути вызова: кто вызвал метод и прошёл ли вызов через обёртку.

Частая ошибка: искать причину в настройках. Аннотация без прокси — просто аннотация, и конфигурация тут ни при чём.

Раздел «Почему @Transactional молча не срабатывает»
JDK-прокси или CGLIB — что будет использовано?

Что проверяют: знаете ли вы, что выбор делается автоматически и зависит от наличия интерфейса.

Частая ошибка: сказать «зависит от конфигурации» и остановиться. Настройкой можно принудить к подклассу, но по умолчанию правило определяется целевым объектом.

Раздел «Границы прокси и что с ними делают»

Проверьте эти темы вслух

В тренажёре вы отвечаете голосом, а разбор показывает, где ответ просел.