Перейти к содержимому

Как Spring создаёт Bean: от BeanDefinition до Proxy

Разбираемся, что происходит между @Service в исходном коде и готовым Spring Bean: от BeanDefinition и BeanFactoryPostProcessor до dependency injection, @PostConstruct, BeanPostProcessor и AOP Proxy.

Возьмём совершенно обычный Spring-сервис:

@Service
public class PaymentService {

    private final PaymentRepository repository;

    public PaymentService(PaymentRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void pay() {
        // ...
    }
}

В другом bean мы просто получаем его через constructor injection:

@Service
public class OrderService {

    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
}

На уровне прикладного кода всё выглядит почти так, будто Spring где-то внутри сделал:

PaymentRepository repository = ...;
PaymentService paymentService =
    new PaymentService(repository);

Но это только небольшой кусок процесса. До вызова конструктора Spring уже должен знать, что такой bean вообще существует и как его создавать. После конструктора ему ещё предстоит выполнить часть dependency injection, вызвать lifecycle callbacks и прогнать объект через инфраструктурные BeanPostProcessor.

Если для PaymentService работает @Transactional, объект, который в итоге получит OrderService, вообще может оказаться не тем экземпляром, который создал конструктор, а proxy вокруг него.

Если отбросить детали, путь получается примерно таким:

@Service / @Bean

BeanDefinition

BeanDefinition post-processing

InstantiationAwareBeanPostProcessor

создание экземпляра

raw Bean

dependency injection

Aware callbacks

BeanPostProcessor
    .postProcessBeforeInitialization()

initialization

BeanPostProcessor
    .postProcessAfterInitialization()

возможно Proxy

exposed Bean

Эту схему дальше и будем постепенно разбирать.

До Bean сначала появляется BeanDefinition

Одна из вещей, которую легко пропустить при изучении Spring: найденный @Service ещё не означает созданный объект.

Когда component scanning обнаруживает:

@Service
public class PaymentService {
}

контейнер сначала регистрирует описание будущего bean. В Spring это описание представлено интерфейсом:

BeanDefinition

Концептуально внутри него находится примерно такая metadata:

beanClass       = PaymentService
scope           = singleton
lazyInit        = false
primary         = false
dependsOn       = ...
factoryMethod   = ...
initMethod      = ...
destroyMethod   = ...

BeanDefinition — не bean, а рецепт его создания. Для дальнейшего разбора достаточно держать в голове простую границу:

BeanDefinition != Bean instance

Пока контейнер работает с definition, конструктор PaymentService ещё мог ни разу не вызываться.

Откуда берётся BeanDefinition

Для:

@Service
@Component
@Repository
@Controller

definition обычно появляется в результате component scanning.

У:

@Configuration
public class PaymentConfiguration {

    @Bean
    PaymentService paymentService(
        PaymentRepository repository
    ) {
        return new PaymentService(repository);
    }
}

механизм другой. Spring анализирует configuration class и регистрирует definition, в котором будет информация о factory method:

beanName:
paymentService

factoryMethod:
paymentService

factoryBean:
paymentConfiguration

Есть XML, программная регистрация и другие способы, но для дальнейшего lifecycle источник уже не так важен. В какой-то момент контейнер получает набор:

paymentService  -> BeanDefinition
orderService    -> BeanDefinition
repository      -> BeanDefinition
dataSource      -> BeanDefinition
...

и только после этого начинает создавать объекты.

Зачем вообще нужен промежуточный BeanDefinition

Можно было бы после обнаружения @Service сразу сделать:

new PaymentService(...);

Но тогда мы потеряли бы довольно большую часть архитектуры Spring: пока существует только definition, инфраструктура может его изменить.

Например:

BeanDefinition definition =
    beanFactory.getBeanDefinition(
        "paymentService"
    );

definition.setLazyInit(true);

Можно поменять scope, класс bean, properties, зависимости, factory method и другую metadata. Кроме того, Spring может зарегистрировать новые definitions, которых в первоначальном наборе вообще не было.

Хороший пример — обработка:

@Configuration

и:

@Bean

Когда configuration classes анализируются, из них появляются дополнительные BeanDefinition.

Перед созданием бизнес-объектов Spring фактически строит модель будущего ApplicationContext. Поэтому два похожих по названию extension point работают с совершенно разными сущностями.

BeanFactoryPostProcessor и BeanPostProcessor — разные уровни

Есть BeanFactoryPostProcessor и BeanPostProcessor. Названия почти одинаковые, из-за чего их легко спутать, но стадии у них разные.

Я бы разделял их так:

BeanFactoryPostProcessor

работает с описанием Bean

BeanPostProcessor

работает с экземпляром Bean

Например:

@Component
public class PaymentBeanConfigurer
    implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(
        ConfigurableListableBeanFactory beanFactory
    ) {
        BeanDefinition definition =
            beanFactory.getBeanDefinition(
                "paymentService"
            );

        definition.setLazyInit(true);
    }
}

В этом коде нам вообще не нужен экземпляр PaymentService: мы изменяем его BeanDefinition.

Если провести бытовую аналогию: BeanFactoryPostProcessor меняет рецепт до приготовления блюда, BeanPostProcessor получает уже приготовленный объект.

На практике такой способ различать их полезнее, чем запоминать два почти одинаковых определения.

BeanDefinitionRegistryPostProcessor

Есть ещё более ранний вариант — BeanDefinitionRegistryPostProcessor. Он позволяет не только менять существующие definitions, но и добавлять новые в registry.

Концептуально startup можно представить так:

первоначальные BeanDefinition

BeanDefinitionRegistryPostProcessor

может добавить новые definitions

BeanFactoryPostProcessor

может изменить definitions

создание обычных Bean

На этом месте хорошо видна роль одного из важных инфраструктурных классов Spring — ConfigurationClassPostProcessor. Он разбирает configuration classes и участвует в регистрации definitions для @Bean, imports и другой configuration metadata.

Название этого класса для обычной прикладной разработки не особенно важно. Полезнее сама модель: Spring configuration сначала превращается в metadata, а объекты создаются позже.

Когда Spring начинает создавать Bean

После подготовки definitions можно переходить к экземплярам.

Для обычных singleton beans без lazy ApplicationContext обычно создаёт их во время startup. Для lazy beans создание можно отложить до первого обращения.

Представим запрос:

PaymentService service =
    context.getBean(PaymentService.class);

Очень упрощённо внутри происходит:

getBean()

singleton уже создан?

   ├── да → вернуть существующий

   └── нет

     создать Bean

Для singleton это особенно заметно: если PaymentService уже существует, Spring не проходит весь lifecycle ещё раз:

constructor
injection
@PostConstruct
proxy creation

Он возвращает сохранённый экземпляр. Если bean ещё не создан — начинается полный lifecycle.

Создание экземпляра

Возьмём снова:

@Service
public class PaymentService {

    private final PaymentRepository repository;

    public PaymentService(
        PaymentRepository repository
    ) {
        this.repository = repository;
    }
}

Чтобы создать объект, Spring должен сначала получить:

PaymentRepository

Если repository уже singleton и создан, задача простая. Если нет, создание PaymentService временно останавливается и начинается создание dependency:

create PaymentService

нужен PaymentRepository

create PaymentRepository

PaymentRepository готов

вернуться к PaymentService

В настоящем приложении цепочка быстро превращается в граф:

OrderController

OrderService

PaymentService

PaymentRepository

EntityManager

DataSource

Считать, что Spring просто «создаёт beans по очереди», не совсем правильно. Порядок сильно определяется зависимостями между ними.

В какой-то момент все constructor arguments разрешены и Spring действительно может сделать концептуальный аналог:

new PaymentService(repository);

После конструктора появляется настоящий Java-object:

PaymentService@4fa...

Lifecycle на этом не закончен. Такой экземпляр дальше удобно называть raw bean, чтобы не путать его с тем объектом, который контейнер в итоге отдаст приложению.

Constructor injection и field injection

С constructor injection всё произошло непосредственно во время создания объекта:

public PaymentService(
    PaymentRepository repository
) {
    this.repository = repository;
}

Но можно написать и так:

@Service
public class PaymentService {

    @Autowired
    private PaymentRepository repository;
}

Здесь порядок другой: сначала constructor создаёт объект

PaymentService
repository = null

а затем Spring должен найти поле и установить dependency.

Если отбросить детали:

instantiate

PaymentService

populate properties

@Autowired

PaymentRepository injected

То же относится к setter injection и части другой property-based конфигурации.

Это легко увидеть и в обычном коде. Если использовать field injection и попытаться обратиться к dependency прямо в constructor:

@Autowired
private PaymentRepository repository;

public PaymentService() {
    repository.findAll();
}

то ожидать готовый repository здесь нельзя: Spring ещё не дошёл до стадии injection. С constructor injection такой проблемы нет в принципе — без dependency объект вообще невозможно создать.

Кто обрабатывает @Autowired

Интуитивно можно представить Spring примерно так:

if (field.isAnnotationPresent(Autowired.class)) {
    inject(field);
}

Архитектурно это устроено интереснее: обработкой @Autowired занимается инфраструктура, в частности AutowiredAnnotationBeanPostProcessor. Значительная часть annotation-based поведения самого Spring построена на тех же extension points, которые доступны разработчику.

BeanPostProcessor поэтому и оказывается одним из центральных механизмов Spring Core. По сути framework получает возможность сказать:

Когда создаётся очередной Bean, дай мне возможность посмотреть на него и выполнить свою дополнительную работу.

AutowiredAnnotationBeanPostProcessor через эту точку ищет injection points и разрешает dependencies. Другие processors используют тот же механизм для своих задач.

Initialization: что происходит после injection

Добавим @PostConstruct:

@Service
public class PaymentService {

    private final PaymentRepository repository;

    public PaymentService(
        PaymentRepository repository
    ) {
        this.repository = repository;
        System.out.println("constructor");
    }

    @PostConstruct
    void init() {
        System.out.println("post construct");
    }
}

К моменту вызова:

@PostConstruct
void init()

dependency injection уже выполнен. Это подходящая точка, чтобы проверить собственную конфигурацию bean или подготовить локальные структуры данных:

@PostConstruct
void init() {
    this.paymentTimeout =
        requirePositive(properties.timeout());
}

Но @PostConstruct — плохое место для тяжёлой работы. repository.checkConnection() уже может означать I/O, а обычные initialization callbacks выполняются во время создания singleton bean. Долгая проверка базы, сетевой вызов или обращение к ещё создаваемому bean способны затормозить startup и в неудачном графе зависимостей привести к deadlock.

Если действие должно начаться после создания всех обычных singleton beans, лучше вынести его, например, в SmartInitializingSingleton:

@Component
public class CacheWarmup
    implements SmartInitializingSingleton {

    private final PaymentRepository repository;

    public CacheWarmup(
        PaymentRepository repository
    ) {
        this.repository = repository;
    }

    @Override
    public void afterSingletonsInstantiated() {
        repository.warmUp();
    }
}

Если собрать основные части initialization, порядок выглядит примерно так:

constructor

dependency injection

Aware callbacks

BeanPostProcessor
    .postProcessBeforeInitialization()

@PostConstruct

InitializingBean.afterPropertiesSet()

custom init-method

BeanPostProcessor
    .postProcessAfterInitialization()

Здесь есть важная терминологическая ловушка: postProcessBeforeInitialization() означает не «до constructor», а до initialization callbacks. Объект к этому моменту уже создан, и dependencies в него в основном уже подставлены.

Несколько init-механизмов

Spring поддерживает несколько вариантов: @PostConstruct, интерфейс InitializingBean и custom init method.

Например:

@Bean(initMethod = "init")
PaymentService paymentService(...) {
    ...
}

Если используются разные методы, порядок будет:

@PostConstruct

afterPropertiesSet()

custom init-method

Я бы при этом не стал специально реализовывать одновременно все три механизма в реальном bean. Это полезно знать как lifecycle, но в application code такой набор обычно только усложняет чтение.

Aware callbacks

До initialization Spring может передать bean информацию о container.

Есть, например:

BeanNameAware
BeanFactoryAware
BeanClassLoaderAware
ApplicationContextAware

Пример:

@Component
public class ContextHolder
    implements ApplicationContextAware {

    private ApplicationContext context;

    @Override
    public void setApplicationContext(
        ApplicationContext applicationContext
    ) {
        this.context = applicationContext;
    }
}

С точки зрения lifecycle это ещё один этап:

dependency injection

Aware callbacks

initialization

В инфраструктурном коде такой доступ бывает нужен. В обычном service я бы уже насторожился, если увидел:

ApplicationContextAware

и дальше:

context.getBean(...)

потому что DI начинает превращаться обратно в ручной Service Locator. Это не запрет, просто хорошая точка для вопроса: почему зависимость нельзя получить обычным способом?

Что происходит при закрытии context

Lifecycle не заканчивается на создании объекта. Когда ApplicationContext закрывается, Spring вызывает destruction callbacks у singleton beans, которыми управляет container:

@PreDestroy

DisposableBean.destroy()

custom destroy-method

Например:

@Component
public class MessageConsumer {

    @PreDestroy
    void stop() {
        client.close();
    }
}

Здесь обычно освобождают connection pool, останавливают consumer, закрывают клиент или завершают фоновую задачу. Если один и тот же метод указан несколькими lifecycle-механизмами, Spring вызовет его один раз, а не несколько.

У prototype bean другая граница ответственности: container создаёт экземпляр и выполняет initialization, но дальше не отслеживает его destruction callbacks. Закрывать такой объект должен код, который его запросил.

@PreDestroy не стоит считать страховкой от любого завершения процесса. Он сработает при штатном закрытии context; при аварийном завершении JVM или принудительной остановке процесса метод может не выполниться.

BeanPostProcessor и момент, когда объект можно заменить

Базовый контракт выглядит очень скромно:

public interface BeanPostProcessor {

    Object postProcessBeforeInitialization(
        Object bean,
        String beanName
    );

    Object postProcessAfterInitialization(
        Object bean,
        String beanName
    );
}

Но самое интересное здесь не callbacks, а возвращаемый тип Object: processor получает один объект и совершенно не обязан вернуть именно его.

Самый простой processor может сделать:

@Override
public Object postProcessAfterInitialization(
    Object bean,
    String beanName
) {
    log.info("Bean created: {}", beanName);

    return bean;
}

Но технически он может сделать:

return anotherObject;

Именно эта возможность позволяет Spring после создания обычного Java-object заменить объект, который увидит остальное приложение. Здесь мы и приходим к proxy.

Откуда берётся @Transactional

Есть:

@Transactional
public void pay() {
    // ...
}

Можно поставить breakpoint внутрь PaymentService и убедиться: никакой специальной transaction-логики в самом методе не появилось. Аннотация @Transactional не превращает исходный метод в:

beginTransaction();

try {
    pay();
    commit();
} catch (...) {
    rollback();
}

внутри самого PaymentService.

При обычном proxy-based AOP Spring идёт другим путём. После создания target

PaymentService@123

AOP infrastructure определяет, что этому bean подходит transaction advisor.

И наружу вместо target может быть отдан:

PaymentServiceProxy@456

target = PaymentService@123

В итоге контейнер может опубликовать уже другую ссылку:

raw bean
PaymentService@123

BeanPostProcessor / AOP infrastructure

exposed bean
PaymentServiceProxy@456

Здесь появляется ещё одна полезная граница:

raw bean != exposed bean

В простом bean они совпадут. При AOP exposed object вполне может оказаться proxy вокруг исходного instance.

Что реально происходит при вызове через Proxy

Другой bean получает:

private final PaymentService paymentService;

но за этой ссылкой может стоять proxy.

Вызов:

paymentService.pay();

тогда проходит примерно такой путь:

OrderService

PaymentService Proxy

TransactionInterceptor

открыть transaction

PaymentService.pay()

commit / rollback

Эта схема сразу объясняет несколько особенностей Spring AOP, и самая известная из них — self invocation.

Есть:

public void process() {
    pay();
}

@Transactional
public void pay() {
}

Если process() уже выполняется внутри target, вызов:

pay();

фактически означает:

this.pay();

Маршрут получается:

PaymentService target

this.pay()

а не:

caller

Proxy

PaymentService.pay()

Proxy в этом вызове вообще не участвовал, а значит, не участвовал и TransactionInterceptor. Поэтому self invocation — не «странная особенность @Transactional», а прямое следствие proxy architecture. Когда несколько раз поймаешь такой баг, смотреть на @Transactional отдельно от call path уже перестаёшь.

@PostConstruct плюс @Transactional

Ещё один хороший пример пересечения нескольких частей lifecycle:

@PostConstruct
void init() {
    loadData();
}

@Transactional
public void loadData() {
    // ...
}

Выглядит убедительно: loadData() помечен @Transactional, значит transaction должна появиться. Но init() выполняется ещё во время initialization target bean, а сам вызов loadData() является обычным self invocation.

То есть:

PaymentService

this.loadData()

не проходит через внешний AOP proxy.

Именно поэтому разбирать такие ошибки удобнее через lifecycle:

На какой стадии находится Bean?
        +
Через какой объект идёт вызов?

Proxy нужен не каждому Bean

Здесь есть и обратная ловушка: из всего сказанного легко решить, что Spring Bean = Proxy. Это тоже неверно.

Обычный:

@Component
public class PriceCalculator {
}

может пройти через все processors:

PriceCalculator@123

BeanPostProcessors

PriceCalculator@123

и остаться тем же экземпляром.

А:

@Service
@Transactional
public class PaymentService {
}

может закончить lifecycle как:

PaymentServiceProxy@456

PaymentService@123

Proxy появляется только тогда, когда соответствующая infrastructure считает, что bean нужно обернуть. Поэтому bean.getClass() не всегда показывает класс, который ожидаешь увидеть по исходному коду.

При диагностике AOP мне обычно полезнее два вопроса:

AopUtils.isAopProxy(bean);

и:

AopUtils.getTargetClass(bean);

Если @Transactional явно должен работать, а первый возвращает false, я бы сначала выяснял, почему bean вообще не попал под auto-proxying, а не лез в настройки transaction manager.

Почему new PaymentService() — уже другой случай

Отсюда уже легко ответить на вопрос:

PaymentService service =
    new PaymentService(repository);

Почему не работает @Transactional? Потому что этот объект вообще не прошёл через container: для него был только constructor. Spring Bean проходит намного более длинный путь:

BeanDefinition

creation

dependency processing

Aware

initialization

BeanPostProcessor

AOP proxy

Аннотация физически остаётся на классе, но никто не построил вокруг объекта infrastructure, которая эту annotation интерпретирует. Та же идея применима не только к @Transactional.

Когда какая-то Spring-аннотация «не работает», один из первых вопросов всегда имеет смысл сформулировать так:

Этот объект вообще создавал Spring?

Ответ экономит довольно много времени.

Где именно Spring делает всё это

Если хочется посмотреть на реализацию, а не только на концепцию, центральным классом здесь будет:

AbstractAutowireCapableBeanFactory

Если очень грубо разложить его создание bean:

createBean()

doCreateBean()

createBeanInstance()

populateBean()

initializeBean()

Названия довольно хорошо описывают содержание: createBeanInstance() отвечает за создание экземпляра, populateBean() — за properties и dependency-related processing, а initializeBean() — за Aware callbacks, before-initialization processors, init methods и after-initialization processors.

Реальный исходник сложнее этой схемы, но после понимания lifecycle его уже можно читать сверху вниз и хотя бы понимать, зачем существует очередной callback. Без этой модели он действительно выглядит как несколько тысяч строк специальных случаев.

Почему BeanPostProcessor создаются раньше обычных Bean

Здесь возникает интересная рекурсия.

Чтобы:

AutowiredAnnotationBeanPostProcessor
AOP AutoProxyCreator
...

могли обработать PaymentService, сами processors должны уже существовать и быть зарегистрированы.

Поэтому инфраструктура создания context условно выглядит так:

создать infrastructure BeanPostProcessor

зарегистрировать их

создавать application Bean

пропускать Bean через processors

Из этого следует довольно неприятный крайний случай: какой-то BeanPostProcessor во время собственного создания может потребовать обычный application bean:

MyBeanPostProcessor

PaymentService

PaymentService может быть создан слишком рано — в момент, когда часть processors ещё не зарегистрирована. В итоге bean вроде существует и dependency injection прошёл, но часть дальнейшей обработки он не получил; например, можно неожиданно остаться без ожидаемого auto-proxying.

В типичном приложении такое встречается редко, но если встретилось, знание порядка lifecycle перестаёт быть теорией для собеседования. Порядок создания infrastructure действительно влияет на результат.

Happy path и реальный Spring — немного разные вещи

Рабочая модель из предыдущих разделов полезна для обычного singleton bean, но реальный Spring умеет вмешиваться в создание раньше и идти по другим веткам. Есть InstantiationAwareBeanPostProcessor, FactoryBean, factory methods, scopes, custom instantiation strategies и, конечно, circular dependencies.

Например:

ServiceA

ServiceB

ServiceA

Обычный алгоритм:

полностью создать A

полностью создать B

уже не работает.

Spring для некоторых singleton cycles использует early references, и тогда появляются:

singletonObjects
earlySingletonObjects
singletonFactories

а при наличии AOP ещё и:

getEarlyBeanReference()

Там становится особенно интересен вопрос, какой объект дать другой стороне цикла:

raw ServiceA

или:

ServiceA Proxy

Но circular dependencies — уже материал для отдельной статьи.

Как я бы искал проблему с конкретным Bean

Когда @Transactional, @Async или другая proxy-based возможность ведёт себя неожиданно, мне обычно удобнее пройти путь bean с конца к началу.

Spring вообще управляет этим объектом?

new PaymentService(...)

и:

context.getBean(PaymentService.class)

— принципиально разные случаи.

Какой объект находится в context?

Object bean =
    context.getBean(PaymentService.class);

System.out.println(bean.getClass());

Есть ли AOP proxy?

AopUtils.isAopProxy(bean);

Какой настоящий target class?

AopUtils.getTargetClass(bean);

Через какой объект вызывается метод?

Внешний вызов:

OrderService

PaymentService Proxy

PaymentService

или self invocation:

PaymentService

this.pay()

Эти несколько проверок обычно сильно сужают область поиска: если proxy вообще нет, обсуждать работу конкретного interceptor пока рано; если proxy есть, но method call идёт через this, причина тоже почти найдена.

Что здесь действительно стоит запомнить

Если уж совсем сжать статью, у bean полезно различать три состояния. BeanDefinition — пока есть только metadata. raw bean — Java-object уже создан, но Spring ещё может его обрабатывать. exposed bean — то, что container в итоге отдаёт остальному приложению.

В виде схемы:

BeanDefinition

raw Bean

exposed Bean

В простом случае:

raw Bean == exposed Bean

При AOP:

raw Bean != exposed Bean

потому что exposed bean — proxy.

Эта модель объясняет заметную часть поведения Spring Core без необходимости воспринимать framework как набор независимых аннотаций.

Вопросы, которыми я бы проверил понимание

  1. Что такое BeanDefinition и зачем Spring отделяет его от экземпляра bean?
  2. Когда Spring создаёт BeanDefinition, а когда сам объект?
  3. Чем BeanFactoryPostProcessor отличается от BeanPostProcessor?
  4. Зачем нужен BeanDefinitionRegistryPostProcessor?
  5. Что примерно делает ConfigurationClassPostProcessor?
  6. Когда выполняется constructor injection?
  7. Чем lifecycle field injection отличается от constructor injection?
  8. За счёт чего Spring обрабатывает @Autowired?
  9. Что раньше: dependency injection или @PostConstruct?
  10. В каком порядке выполняются @PostConstruct, afterPropertiesSet() и custom init method?
  11. Когда вызываются Aware callbacks?
  12. Может ли BeanPostProcessor вернуть другой объект вместо исходного?
  13. Где обычно появляется Spring AOP Proxy?
  14. Каждый ли Spring Bean является Proxy?
  15. Что такое raw bean и exposed bean?
  16. Почему new PaymentService() не получает обычное Spring AOP поведение?
  17. Почему self invocation не проходит через proxy?
  18. Почему вызов @Transactional метода из @PostConstruct подозрителен?
  19. Зачем BeanPostProcessor создаются раньше обычных application beans?
  20. Почему circular dependency усложняет обычный bean lifecycle?