Урок по треку Spring и Spring Boot 7 мин

Бины: скоупы и жизненный цикл

Singleton и prototype, web-скоупы, @PostConstruct, BeanPostProcessor и порядок создания бинов.

Вопросы о бинах проверяют, понимаете ли вы, что происходит при старте контекста: сколько экземпляров создаётся, когда вызывается @PostConstruct, откуда берутся прокси и почему prototype внутри singleton «не работает».

Скоупы

СкоупЭкземплярГде
singleton (по умолчанию)один на контекствезде
prototypeновый при каждом запросе бина из контейнеравезде
requestодин на HTTP-запросвеб
sessionодин на HTTP-сессиювеб
applicationодин на ServletContextвеб
websocketодин на WebSocket-сессиювеб

Singleton в Spring это «один на контекст», а не паттерн Singleton из GoF: можно создать два бина одного класса с разными именами, и это будут два объекта.

Singleton-бин используется всеми потоками сразу, поэтому он должен быть без изменяемого состояния (stateless) или потокобезопасным. Поле-счётчик или «текущий пользователь» в поле сервиса это гонка.

Prototype внутри singleton

@Component @Scope("prototype")
class ReportBuilder { ... }

@Service
class ReportService {
    private final ReportBuilder builder;   // внедрится ОДИН раз
    ReportService(ReportBuilder builder) { this.builder = builder; }
}

Зависимость внедряется один раз при создании singleton, поэтому «новый экземпляр каждый раз» не получится. Решения: ObjectProvider<ReportBuilder> и getObject() на каждый вызов, метод с @Lookup, или scoped proxy (@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)). Для request и session бинов в singleton Spring требует scoped proxy, он сам по себе ищет бин текущего запроса.

Ещё одна особенность prototype: Spring не управляет его уничтожением, @PreDestroy у него не вызывается.

Жизненный цикл бина

  1. Создание экземпляра (конструктор, внедрение через конструктор).
  2. Внедрение зависимостей в поля и сеттеры.
  3. *Aware колбэки (BeanNameAware, ApplicationContextAware).
  4. BeanPostProcessor.postProcessBeforeInitialization.
  5. Инициализация: @PostConstruct, затем InitializingBean.afterPropertiesSet(), затем initMethod из @Bean.
  6. BeanPostProcessor.postProcessAfterInitialization: здесь создаются прокси для @Transactional, @Async, @Cacheable, AOP.
  7. Бин готов к работе.
  8. При закрытии контекста: @PreDestroy, DisposableBean.destroy(), destroyMethod.

Важное следствие: в конструкторе и в @PostConstruct вы работаете с самим объектом, а не с прокси. Вызов @Transactional-метода из @PostConstruct через this транзакцию не откроет. Для логики «после полного старта» есть ApplicationReadyEvent или SmartInitializingSingleton.

@PostConstruct и @PreDestroy теперь в пакете jakarta.annotation (Spring 6 и Boot 3 перешли с javax на jakarta).

BeanPostProcessor и BeanFactoryPostProcessor

  • BeanFactoryPostProcessor работает до создания бинов и меняет их определения (BeanDefinition). Пример: подстановка ${...} из свойств (PropertySourcesPlaceholderConfigurer).
  • BeanPostProcessor работает с готовыми экземплярами, может обернуть бин в прокси. Пример: AutowiredAnnotationBeanPostProcessor, создатель AOP-прокси.

Ленивая инициализация

Синглтоны создаются при старте, ошибки видны сразу. @Lazy на бине откладывает создание до первого обращения. Глобально spring.main.lazy-initialization=true ускоряет старт, но переносит ошибки конфигурации на первый запрос.

Частые ошибки

  • Изменяемое состояние в singleton-сервисе.
  • Ожидание нового prototype при каждом вызове метода singleton.
  • Вызов транзакционного или асинхронного метода из @PostConstruct.
  • Тяжёлая работа в конструкторе (сетевые вызовы), из-за которой контекст стартует минуты.

Что спросят на собесе

  • Какие скоупы бинов есть, какой по умолчанию?
  • Потокобезопасен ли singleton-бин?
  • Что будет, если внедрить prototype в singleton, и как получить новый экземпляр?
  • Расскажите жизненный цикл бина. Где создаются прокси?
  • Чем BeanPostProcessor отличается от BeanFactoryPostProcessor?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.