Разбираем Spring: бины, контекст и их жизненный цикл
Когда начинаешь работать со Spring, довольно быстро появляется ощущение, что фреймворк берёт на себя почти всё: создаёт объекты, связывает их, управляет зависимостями. На старте это сильно ускоряет разработку: можно не думать о том, как именно создаются объекты и как они передаются друг другу. Но по мере роста проекта это упрощение начинает играть против тебя: поведение становится менее очевидным, а ошибки — менее предсказуемыми. В какой-то момент приходится разбираться, что именно делает Spring и по каким правилам он это делает.
В основе всего лежит IoC-контейнер — механизм, который управляет созданием объектов (бинов) и их зависимостями. Вместо того чтобы вручную контролировать жизненный цикл объектов, разработчик делегирует это контейнеру. Это меняет сам подход к архитектуре: зависимости описываются декларативно, а не создаются напрямую. И как только ты начинаешь воспринимать Spring именно как систему управления объектами, многие вещи становятся логичными.
Контекст: где на самом деле “живёт” приложение
ApplicationContext — это центральная точка, через которую проходит всё. Он не просто хранит бины, а управляет их созданием, зависимостями и жизненным циклом. Фактически, это и есть “собранное” приложение.
При инициализации контекста происходит сразу несколько важных процессов:
Считывается конфигурация
Строится граф зависимостей
Создаются singleton-бины
Выполняется внедрение зависимостей
Большая часть этой работы происходит именно на старте, а не в момент первого вызова. Это важно учитывать, потому что любые тяжёлые операции в конструкторах или init-методах напрямую влияют на время запуска приложения.
На практике чаще всего используются:
AnnotationConfigApplicationContext
WebApplicationContext
Scope: почему бин не всегда ведёт себя так, как ожидаешь
Scope определяет, сколько экземпляров бина существует внутри контейнера. В базовом виде:
singleton — один экземпляр на контекст
prototype — новый экземпляр при каждом запросе
request / session — для веба
На уровне декларации всё выглядит однозначно, но в реальности поведение зависит от того, как бин используется. Особенно это заметно, когда бин участвует в цепочке зависимостей, а не запрашивается напрямую из контекста.
Lifecycle: что происходит с бином после создания
Жизненный цикл бина включает несколько этапов: создание, внедрение зависимостей, инициализация, работа и уничтожение. В большинстве случаев разработчик взаимодействует только с частью этих этапов, но понимать их порядок важно, особенно если бин зависит от внешних ресурсов или требует дополнительной настройки.
На практике чаще всего используются аннотации:
@PostConstruct — логика после внедрения зависимостей
@PreDestroy — освобождение ресурсов
Помимо этого, в Spring достаточно часто используются BeanPostProcessor — механизм, который позволяет вмешиваться в процесс создания бинов и модифицировать их до и после инициализации. Это уже более продвинутый уровень, но он лежит в основе многих вещей внутри самого Spring, включая прокси, AOP и различные автоконфигурации.
Фактически это точка расширения, через которую можно повлиять на любой бин в приложении. Даже если напрямую такие вещи не используются, важно понимать, что lifecycle в Spring — это не фиксированная последовательность, а расширяемый процесс.
Конфигурация: как контейнер узнаёт, что создавать
Spring поддерживает несколько способов конфигурации, и в реальных проектах они часто комбинируются:
XML — чаще встречается в legacy
Java Config — даёт явный контроль
аннотации — основной современный подход
или
Аннотации ускоряют разработку, но могут скрывать структуру приложения. Java Config, наоборот, делает зависимости более явными, поэтому часто используется в сложных сценариях или при необходимости тонкой настройки.
Практический кейс: где ломается ожидание
Рассмотрим типичную ситуацию:
На уровне ожиданий всё выглядит логично: новый экземпляр калькулятора при каждом вызове. Но в реальности используется один и тот же объект. Это происходит потому, что Spring внедряет зависимости на этапе создания бина, а не во время выполнения метода. В данном случае OrderService создаётся один раз (как singleton), и вместе с ним создаётся и внедряется конкретный экземпляр DiscountCalculator. То есть несмотря на то, что у калькулятора указан prototype, он не пересоздаётся при каждом вызове. Контейнер просто не инициирует этот процесс повторно. Именно здесь часто возникает рассинхрон между ожиданиями и фактическим поведением.
Дополнительно стоит обратить внимание на сам способ внедрения зависимости. В примере используется @Autowired, но это не единственный вариант. В Spring есть несколько способов:
Через поле (@Autowired) — быстро, но менее прозрачно
Через конструктор — наиболее предпочтительный способ
Через setter — используется реже, чаще для опциональных зависимостей
На практике сейчас чаще всего выбирают конструкторную инъекцию, потому что она делает зависимости явными и упрощает тестирование. Полевая инъекция остаётся популярной за счёт краткости, но хуже масштабируется в сложных системах.
Если в этом кейсе действительно требуется новый экземпляр при каждом вызове, приходится явно обращаться к контейнеру (например, через ObjectProvider), что уже выводит разработчика на более осознанный уровень работы со Spring.
Где чаще всего ошибаются
Чаще всего проблемы возникают не из-за самого Spring, а из-за неверных ожиданий от него. Обычно это:
Неправильное понимание scope
Игнорирование lifecycle
Неявные зависимости из-за выбранного способа инъекции
И почти всегда за этим стоит попытка воспринимать Spring как “магический слой”, который сам решает архитектурные задачи.
Пока приложение небольшое, многие детали можно игнорировать, всё действительно работает “из коробки”. Но по мере роста сложности поведение контейнера начинает напрямую влиять на систему. Это касается и многопоточности, и управления состоянием, и работы с ресурсами.
В такие моменты становится очевидно, что Spring не упрощает систему сам по себе, а просто централизует управление ею. И чем лучше ты понимаешь, как устроены context, scope и lifecycle, тем предсказуемее становится поведение приложения.
Хотите узнать больше? Изучите другие статьи из раздела: