Возьмём совершенно обычный 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 как набор независимых аннотаций.
Вопросы, которыми я бы проверил понимание
- Что такое
BeanDefinitionи зачем Spring отделяет его от экземпляра bean? - Когда Spring создаёт
BeanDefinition, а когда сам объект? - Чем
BeanFactoryPostProcessorотличается отBeanPostProcessor? - Зачем нужен
BeanDefinitionRegistryPostProcessor? - Что примерно делает
ConfigurationClassPostProcessor? - Когда выполняется constructor injection?
- Чем lifecycle field injection отличается от constructor injection?
- За счёт чего Spring обрабатывает
@Autowired? - Что раньше: dependency injection или
@PostConstruct? - В каком порядке выполняются
@PostConstruct,afterPropertiesSet()и custom init method? - Когда вызываются Aware callbacks?
- Может ли
BeanPostProcessorвернуть другой объект вместо исходного? - Где обычно появляется Spring AOP Proxy?
- Каждый ли Spring Bean является Proxy?
- Что такое raw bean и exposed bean?
- Почему
new PaymentService()не получает обычное Spring AOP поведение? - Почему self invocation не проходит через proxy?
- Почему вызов
@Transactionalметода из@PostConstructподозрителен? - Зачем
BeanPostProcessorсоздаются раньше обычных application beans? - Почему circular dependency усложняет обычный bean lifecycle?