Жизненный цикл бина в Spring и зачем нужны прокси
Что на самом деле происходит при старте контекста
Когда говорят «Spring поднял контекст», за этим скрывается конвейер из десятка шагов для каждого бина. На собесе эту тему любят, потому что она сразу вскрывает понимание DI, AOP и типичных багов вроде «почему @Transactional не сработал при вызове метода из этого же класса».
Упрощённый жизненный цикл бина:
- BeanDefinition регистрируется в реестре — это ещё не объект, а метаданные: класс, scope, зависимости, конструктор.
- BeanFactory создаёт экземпляр — вызывает конструктор (или фабричный метод).
- Внедряются зависимости — через конструктор, сеттеры или поля (
@Autowired). - Вызываются Aware-интерфейсы — например,
BeanNameAware,ApplicationContextAware. BeanPostProcessor.postProcessBeforeInitialization— здесь, в частности, применяются некоторые аннотации.@PostConstruct/InitializingBean.afterPropertiesSet()/ кастомныйinit-method.BeanPostProcessor.postProcessAfterInitialization— именно тут чаще всего оборачивают бин в прокси (AOP,@Transactional,@Async,@Cacheable).- Бин готов к использованию и живёт в контексте (если singleton).
- При закрытии контекста —
@PreDestroy/DisposableBean.destroy().
Ключевой момент: то, что лежит в контексте и что вы получаете через @Autowired, — это не всегда «сырой» объект вашего класса. Если на бине есть хотя бы одна аннотация, требующая AOP, в контексте окажется прокси.
JDK-прокси vs CGLIB
Spring AOP умеет создавать прокси двумя способами:
- JDK Dynamic Proxy — работает только через интерфейсы. Прокси реализует тот же интерфейс, что и целевой класс, и делегирует вызовы. Требование: бин должен иметь интерфейс, иначе этот способ не применим.
- CGLIB — генерирует подкласс целевого класса на лету и переопределяет методы. Работает без интерфейсов, но не может проксировать
finalклассы иfinal/privateметоды — они просто не переопределяются, и логика прокси (транзакция, кэш) на них не сработает.
Начиная со Spring Boot 2.x по умолчанию используется CGLIB даже при наличии интерфейсов (настройка proxyTargetClass=true), потому что так проще избежать проблем с приведением типов.
final или сделать класс final и удивляться, что @Transactional не работает. CGLIB физически не может создать подкласс финального класса и молча не применяет advice — ни ошибки, ни предупреждения в рантайме можно не увидеть.Self-invocation: классика собеседований
Самый частый вопрос по теме — почему вызов аннотированного метода изнутри того же класса не проходит через прокси:
@Service
public class OrderService {
public void process(Order order) {
// this - сырой объект, не прокси
save(order);
}
@Transactional
public void save(Order order) {
repository.save(order);
}
}
Вызов save(order) внутри process — это обычный вызов метода через this, минуя прокси. AOP-логика (открытие транзакции) просто не выполнится, потому что перехват происходит на уровне внешнего вызова бина из контейнера, а не внутри самого объекта.
Способы обойти: вынести save в отдельный бин и вызывать его через внедрённую зависимость; либо получитьself-прокси через AopContext.currentProxy() (требует exposeProxy=true); либо использовать ApplicationContext.getBean(...) для получения прокси-версии себя же — редко оправдано, но работает.
Порядок BeanPostProcessor и зависимость от bean definition
Важно понимать: BeanPostProcessor-ы сами регистрируются как бины и обрабатываются раньше обычных бинов — иначе был бы хаос с курицей и яйцом. Spring гарантирует, что инфраструктурные процессоры (например, отвечающие за @Autowired, за AOP-прокси) инициализируются в первую очередь, до бизнес-бинов.
this идёт напрямую к целевому объекту, минуя прокси, поэтому advice (транзакция, асинхронность, кэш) не применяется. Нужно вызывать метод через внедрённую зависимость на другой бин или через self-прокси.