Бины Spring: цикл и скоупы
Повесил на бин отметки во всех точках и поднял контекст. Порядок вышел такой: конструктор (зависимость ещё null), внедрение зависимости, обработчик ДО инициализации, @PostConstruct, afterPropertiesSet, обработчик ПОСЛЕ инициализации, и только потом бин готов. При закрытии контекста - @PreDestroy и destroy.
Шестой шаг тут ключевой: именно там бин подменяют на прокси. Понимать этот конвейер нужно, чтобы объяснять, почему @Transactional иногда молча не срабатывает.
// Формулировки: «опиши жизненный цикл бина», «чем singleton отличается от prototype?», «почему @Transactional не работает при вызове через this?».
Конвейер создания бина
Вот замер целиком, по шагам: 1 - конструктор, поле зависимости ещё null; 2 - внедрение зависимости; 3 - BeanPostProcessor до инициализации; 4 - @PostConstruct, зависимость уже на месте; 5 - afterPropertiesSet; 6 - BeanPostProcessor после инициализации; 7 - контекст поднят. При закрытии: 8 - @PreDestroy, 9 - destroy.
Первый шаг объясняет частую ошибку: инициализацию, которой нужны зависимости, нельзя делать в конструкторе - там половина полей ещё пустая. Для этого есть @PostConstruct, он выполняется уже после внедрения.
BeanPostProcessor - это перехватчик, которому контейнер показывает каждый создаваемый бин и разрешает вернуть вместо него что-то другое. Именно здесь, на шаге 6, бин заменяется на прокси - обёртку, которая снаружи выглядит как твой объект, но перед вызовом метода успевает открыть транзакцию, положить результат в кэш или отправить вызов в другой поток. Нет обёртки - нет ни транзакций, ни кэша.
- @PostConstruct
- инициализация после внедрения зависимостей, а не в конструкторе
- прокси
- обёртка вокруг бина, которая вклинивается перед вызовом метода
Скоупы: сколько экземпляров живёт
Скоуп - это ответ на вопрос «сколько экземпляров бина существует». По умолчанию singleton: один на весь контейнер, общий для всех запросов и потоков. Я проверил - два обращения к контейнеру вернули один и тот же объект.
Отсюда жёсткое требование: singleton не должен хранить изменяемое состояние. Поле-накопитель в таком сервисе под нагрузкой перемешает данные разных пользователей, потому что объект у них общий. Есть ещё prototype - новый экземпляр на каждое обращение, и веб-скоупы request и session, живущие на один HTTP-запрос и на сессию.
И вот ловушка, которую я замерил. Внедрил prototype-бин полем в singleton и обратился к нему трижды: каждый раз приходил один и тот же экземпляр номер 1. Ничего удивительного - поле заполнили однажды, при создании владельца. Рядом взял тот же бин через ObjectProvider - объект, у которого можно попросить экземпляр в момент вызова: пришли номера 2, 3 и 4. Всего создалось четыре экземпляра.
class Holder {
@Autowired Proto injectedField; // prototype полем
@Autowired ObjectProvider<Proto> provider; // он же через провайдера
}
// обращение 1: поле -> Proto#1, провайдер -> Proto#2
// обращение 2: поле -> Proto#1, провайдер -> Proto#3
// обращение 3: поле -> Proto#1, провайдер -> Proto#4- singleton / prototype
- один экземпляр на контейнер / новый на каждое обращение
- ObjectProvider
- попросить свежий экземпляр в момент вызова, а не при создании владельца
Два вида прокси и обход через this
Посмотрел, во что превратились бины. Класс без интерфейса стал Orders$$SpringCGLIB$$0 - это сгенерированный ПОДКЛАСС, и проверка instanceof на исходный класс проходит. Класс с интерфейсом стал $Proxy18 - отдельный объект, реализующий тот же интерфейс, и экземпляром исходного класса он уже не является.
Отсюда ограничения. Подкласс нельзя построить для final-класса и нельзя переопределить final-метод, значит перехватить их не выйдет. А прокси по интерфейсу не даст запросить бин по конкретному классу.
Главное следствие - self-invocation, вызов собственного метода через this. Внешний вызов идёт через прокси: он открывает транзакцию и передаёт управление твоему методу. А this.save() внутри того же класса обращается к объекту напрямую, минуя обёртку. Я это замерил: при внешнем вызове в логе появились открытие и коммит, при вызове через this - НИЧЕГО. Ни ошибки, ни транзакции.
@Service
class Orders {
@Transactional void save(Order o) { ... }
void batch(List<Order> os) {
for (Order o : os) this.save(o); // мимо прокси
}
}
// вызов снаружи: BEGIN ... COMMIT
// вызов через this: в логе пусто- self-invocation
- вызов своего метода через this идёт мимо прокси, аннотация не работает
- CGLIB / JDK-прокси
- подкласс, когда интерфейса нет / отдельный объект по интерфейсу
Как отвечать: «Почему @Transactional не работает при вызове this.method()?»
Потому что аннотация живёт не в самом объекте, а в прокси - обёртке, которой Spring подменяет бин на шестом шаге создания, в обработчике после инициализации. Когда метод зовут снаружи, через ссылку на бин, вызов проходит через обёртку: она открывает транзакцию и передаёт управление настоящему методу. Когда я внутри класса пишу this.save, я обращаюсь к объекту напрямую, обёртка не задействована - и транзакции нет. Причём молча: я проверял на живом контексте, при внешнем вызове в логе есть открытие и коммит, при вызове через this в логе пусто, никакой ошибки. То же самое касается @Async и @Cacheable - механизм один. Лечу выносом метода в отдельный бин, который вызываю снаружи, либо перестраиваю так, чтобы транзакционным был публичный метод-вход.
Почему это сильный ответ: назван корень через устройство прокси и момент его появления, показано, что проблема общая для всех прокси-аннотаций, и дано лечение.
На чём валят
- −Изменяемое поле-накопитель в singleton-сервисе. Объект общий для всех, поэтому под нагрузкой данные пользователей перемешаются.
- −Инициализация в конструкторе вместо @PostConstruct. На первом шаге зависимости ещё null - я это замерил.
- −Prototype-бин полем в singleton. Экземпляр создаётся один раз вместе с владельцем; свежий даёт ObjectProvider.
- −@Transactional или @Async на вызове через this. Прокси обойдён, поведение пропало без единого сообщения.
- −final-класс или final-метод там, где нужен прокси-подкласс. Переопределить нечего, перехват не построится.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Потокобезопасен ли singleton-бин Spring по умолчанию?A)Да, Spring автоматически синхронизирует все методы singleton-бина, поэтому гонок в нём встречается редкоB)Нет — один экземпляр делят все потоки; за потокобезопасность отвечает разработчикC)Да, потому что каждый поток при обращении получает свою отдельную копию singleton-бина из контейнераD)Вопрос неприменим: singleton-бины в Spring не вызываются из нескольких потоков сразу
показать ответ и разбор
+B)Нет — один экземпляр делят все потоки; за потокобезопасность отвечает разработчик// разбор: Spring-скоуп singleton != потокобезопасность. Это ОДИН экземпляр, разделяемый ВСЕМИ потоками (веб-запросы обслуживаются параллельно). Spring ничего не синхронизирует автоматически. Поэтому если в singleton-бине хранить изменяемое состояние (поля, меняющиеся между запросами), возникнут гонки. Правильный дизайн — делать сервисы БЕЗ СОСТОЯНИЯ (stateless): всё нужное передавать в параметрах/локальных переменных. Изменяемое состояние на запрос — в request-scope бинах или локально, а не в полях синглтона.
- Когда используют scope prototype?A)Когда бин должен существовать в одном экземпляре и переиспользоваться во всём приложенииB)Когда бин обслуживает ровно один HTTP-запрос и уничтожается по его завершении контейнеромC)Когда нужен новый экземпляр бина на каждое получение/внедрениеD)Когда бин обязан создаваться заранее при старте контейнера, ещё до первого обращения к нему
показать ответ и разбор
+C)Когда нужен новый экземпляр бина на каждое получение/внедрение// разбор: prototype-бин контейнер создаёт ЗАНОВО при каждом получении (getBean/внедрении). Подходит для объектов с состоянием, которые нельзя разделять, или для короткоживущих сущностей. Нюанс: для prototype контейнер НЕ управляет полным жизненным циклом — не вызывает destroy-колбэки (за очистку отвечает получатель). И осторожно с внедрением prototype в singleton: по умолчанию внедрится ОДИН экземпляр на всё время жизни синглтона (нужен ObjectProvider/@Lookup/scoped proxy, чтобы получать свежий).
- Что делают @PostConstruct и @PreDestroy?A)Создают и удаляют бин: @PostConstruct вызывает конструктор, а @PreDestroy освобождает память объектаB)Помечают методы, которые контейнер выполняет на КАЖДЫЙ вызов бина — до и после основной логики методаC)Задают порядок создания бинов: @PostConstruct-бины создаются последними, @PreDestroy — первымиD)Колбэки жизненного цикла: после внедрения зависимостей и перед уничтожением бина
показать ответ и разбор
+D)Колбэки жизненного цикла: после внедрения зависимостей и перед уничтожением бина// разбор: @PostConstruct — метод, который контейнер вызывает ОДИН раз ПОСЛЕ создания бина и внедрения всех зависимостей (конструктор уже отработал, но там зависимости ещё не гарантированы) — для инициализации. @PreDestroy — метод, вызываемый перед уничтожением бина при закрытии контекста — для освобождения ресурсов (закрыть пул, остановить поток). Это часть lifecycle-колбэков (наряду с InitializingBean/DisposableBean и init/destroy-методами @Bean). Для prototype @PreDestroy НЕ вызывается.
- Чем @Bean-метод в @Configuration отличается от @Component?A)@Bean регистрирует объект программно в конфиг-классе (для чужих/сложных классов); @Component — сканированиемB)@Bean и @Component взаимоисключающи: класс с @Bean-методом не получится одновременно помечать @ConfigurationC)@Component создаёт prototype-бины, а @Bean — только singleton, в этом их основное различиеD)@Bean работает только для интерфейсов, а @Component — только для конкретных реализующих их классов
показать ответ и разбор
+A)@Bean регистрирует объект программно в конфиг-классе (для чужих/сложных классов); @Component — сканированием// разбор: @Component вешают на СВОЙ класс, и component scan сам регистрирует его как бин. @Bean — метод в @Configuration-классе, который ЯВНО создаёт и возвращает объект-бин; так регистрируют то, что нельзя пометить аннотацией: классы из СТОРОННИХ библиотек, объекты со сложной сборкой/настройкой, несколько бинов одного типа. Важно: @Configuration проксируется (CGLIB), поэтому повторный вызов @Bean-метода изнутри вернёт ТОТ ЖЕ синглтон, а не новый объект. Оба подхода дают бины контейнера.
- Когда бин со scope singleton создаётся в ApplicationContext по умолчанию?A)Лениво, только при первом обращении к нему из кода, чтобы ускорить старт приложения по умолчаниюB)Заранее (eager), при инициализации контекста на старте приложенияC)При каждом обращении заново, но результат кэшируется в контейнере до перезапуска приложенияD)Автоматически не создаётся: singleton-бины нужно создавать вручную вызовом context.createBean()
показать ответ и разбор
+B)Заранее (eager), при инициализации контекста на старте приложения// разбор: ApplicationContext по умолчанию создаёт все singleton-бины ЗАРАНЕЕ (eager), при старте контекста. Плюс: ошибки конфигурации (нет зависимости, циклы, падение @PostConstruct) обнаруживаются НЕМЕДЛЕННО, а не при первом запросе в проде. Ленивую инициализацию включают точечно @Lazy (или глобально spring.main.lazy-initialization) — она ускоряет старт, но откладывает проявление ошибок. prototype же всегда создаётся по требованию. По умолчанию «fail fast» на старте предпочитают отложенным сюрпризам.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.