Жизненный цикл бина 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?
Что проверяют: знаете ли вы, что постобработчик может подменить объект целиком. Без этого не объясняется, откуда вообще берутся прокси.
Частая ошибка: ответить «для расширения функциональности». Формулировка правильная и пустая: она подходит к любому интерфейсу расширения и не показывает, что вы понимаете механизм.
Метод помечен аннотацией, но она не сработала. Почему?
Что проверяют: отсутствие ошибки здесь само служит диагностическим признаком. Рассуждение начинается с вопроса о пути вызова: кто вызвал метод и прошёл ли вызов через обёртку.
Частая ошибка: искать причину в настройках. Аннотация без прокси — просто аннотация, и конфигурация тут ни при чём.
JDK-прокси или CGLIB — что будет использовано?
Что проверяют: знаете ли вы, что выбор делается автоматически и зависит от наличия интерфейса.
Частая ошибка: сказать «зависит от конфигурации» и остановиться. Настройкой можно принудить к подклассу, но по умолчанию правило определяется целевым объектом.
Проверьте эти темы вслух
В тренажёре вы отвечаете голосом, а разбор показывает, где ответ просел.