Главная › Блог › Жизненный цикл бина в Spring и зачем нужны прокси

Жизненный цикл бина в Spring и зачем нужны прокси

Что на самом деле происходит при старте контекста

Когда говорят «Spring поднял контекст», за этим скрывается конвейер из десятка шагов для каждого бина. На собесе эту тему любят, потому что она сразу вскрывает понимание DI, AOP и типичных багов вроде «почему @Transactional не сработал при вызове метода из этого же класса».

Упрощённый жизненный цикл бина:

  1. BeanDefinition регистрируется в реестре — это ещё не объект, а метаданные: класс, scope, зависимости, конструктор.
  2. BeanFactory создаёт экземпляр — вызывает конструктор (или фабричный метод).
  3. Внедряются зависимости — через конструктор, сеттеры или поля (@Autowired).
  4. Вызываются Aware-интерфейсы — например, BeanNameAware, ApplicationContextAware.
  5. BeanPostProcessor.postProcessBeforeInitialization — здесь, в частности, применяются некоторые аннотации.
  6. @PostConstruct / InitializingBean.afterPropertiesSet() / кастомный init-method.
  7. BeanPostProcessor.postProcessAfterInitialization — именно тут чаще всего оборачивают бин в прокси (AOP, @Transactional, @Async, @Cacheable).
  8. Бин готов к использованию и живёт в контексте (если singleton).
  9. При закрытии контекста — @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-прокси) инициализируются в первую очередь, до бизнес-бинов.

Почему @Async или @Transactional не срабатывают при вызове метода внутри того же класса?
Потому что AOP в Spring по умолчанию работает через прокси, который оборачивает бин снаружи. Вызов метода через this идёт напрямую к целевому объекту, минуя прокси, поэтому advice (транзакция, асинхронность, кэш) не применяется. Нужно вызывать метод через внедрённую зависимость на другой бин или через self-прокси.
В чём разница между BeanFactory и ApplicationContext в контексте жизненного цикла?
BeanFactory — базовый контейнер с ленивой инициализацией бинов по запросу. ApplicationContext расширяет его: добавляет eager-инициализацию singleton-бинов при старте, события, интернационализацию и автоматическую регистрацию BeanPostProcessor-ов и BeanFactoryPostProcessor-ов из контекста.

Ещё разборы