Это первая тема в Spring и основа всего остального. Разберём с нуля: зачем нужен контейнер, как он создаёт и связывает объекты, и где тут подводные камни.
Что такое IoC и DI простыми словами
Обычно объект сам создаёт то, что ему нужно:
class OrderService {
private final PaymentClient payment = new PaymentClient(); // создаём сами
}
Проблема: OrderService намертво привязан к конкретному PaymentClient. Его не подменить в тесте, не настроить из конфигурации, не переиспользовать.
Inversion of Control (инверсия управления) — идея «не создавай зависимости сам, пусть их даст кто-то снаружи». Управление созданием объектов уходит из вашего кода в контейнер.
Dependency Injection (внедрение зависимостей) — конкретный приём, которым Spring реализует IoC: контейнер сам создаёт PaymentClient и передаёт его в OrderService.
@Service
class OrderService {
private final PaymentClient payment;
OrderService(PaymentClient payment) { this.payment = payment; } // нам его дали
}
Короткая формула: IoC — это принцип, DI — это паттерн, которым его реализуют. Контейнер Spring — тот, кто всё это делает.
ApplicationContext и BeanFactory
Контейнер Spring существует в двух видах:
BeanFactory— базовый контейнер. Умеет читать описания объектов и создавать их по запросу. Напрямую им почти не пользуются.ApplicationContext— этоBeanFactoryплюс всё нужное в реальном приложении: события, работа со свойствами (Environment), сообщения для локализации, загрузка ресурсов. Именно его создаёт Spring Boot при старте.
@SpringBootApplication
public class App {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(App.class, args);
}
}
Объекты, которыми управляет контейнер, называются бинами (beans). При старте Spring сканирует пакеты, находит классы с @Component (и @Service, @Repository, @Controller — это его частные случаи), создаёт из них бины и связывает между собой.
Способы внедрения и почему выбирают конструктор
Внедрить зависимость можно тремя способами:
// 1. Через конструктор — рекомендуется
@Service
class OrderService {
private final PaymentClient payment;
OrderService(PaymentClient payment) { this.payment = payment; }
}
// 2. Через поле — коротко, но проблемно
@Service
class OrderService {
@Autowired private PaymentClient payment;
}
// 3. Через сеттер — для необязательных зависимостей
@Service
class OrderService {
private PaymentClient payment;
@Autowired void setPayment(PaymentClient p) { this.payment = p; }
}
Почему обычно выбирают конструктор:
- поле можно сделать
final— объект нельзя оставить «недостроенным»; - видно все зависимости сразу: если их в конструкторе десять — класс делает слишком много (это полезный сигнал);
- легко тестировать — объект создаётся обычным
newс моками, без запуска Spring; - циклические зависимости вскрываются сразу, а не прячутся (об этом ниже).
С одним конструктором аннотация @Autowired не нужна — Spring и так его использует.
Scopes — сколько живёт бин
Scope определяет, сколько экземпляров бина создаёт контейнер. Встроенных шесть, на практике важны два:
singleton(по умолчанию) — один экземпляр на весь контейнер. Так живут почти все бины:@Service,@Repository,@Controller. Поэтому бины делают без изменяемого состояния (stateless) — один экземпляр обслуживает все запросы параллельно.prototype— новый экземпляр на каждый запрос бина. Spring создаёт его и «отпускает»: метод уничтожения (@PreDestroy) у prototype не вызывается.
Ещё есть области для веб-приложений: request (новый бин на каждый HTTP-запрос), session (на сессию пользователя), application и websocket.
Как внедрить prototype в singleton
Частая ловушка. Если просто внедрить prototype-бин в singleton, он внедрится один раз — при создании singleton'а, и «новизна» потеряется. Чтобы получать свежий экземпляр на каждый вызов, берут ObjectProvider:
@Service
class OrderService {
private final ObjectProvider<RequestContext> provider; // RequestContext — prototype
OrderService(ObjectProvider<RequestContext> p) { this.provider = p; }
void process() {
RequestContext ctx = provider.getObject(); // новый экземпляр каждый раз
}
}
Жизненный цикл бина
От создания до остановки бин проходит несколько фаз:
- создание (вызов конструктора);
- внедрение зависимостей;
- колбэки инициализации —
@PostConstruct, затемafterPropertiesSet(); - обёртывание в прокси (если нужно — например, для
@Transactional); - бин готов к работе;
- при остановке приложения —
@PreDestroy(только для singleton).
Главное практическое следствие: в конструкторе нельзя пользоваться зависимостями, которые внедряются в поля — они ещё не установлены. Логику «после сборки» кладут в @PostConstruct. Это ещё одна причина внедрять через конструктор: к моменту его выполнения все зависимости уже на месте.
Детальный разбор каждой фазы с демо-кодом и ловушками — в отдельной статье: Жизненный цикл Spring-бина с примерами.
@Component и @Bean
Два способа объявить бин, и разницу часто спрашивают:
@Component(и@Service/@Repository/@Controller) ставят на свой класс — Spring найдёт его при сканировании и создаст сам.@Beanставят на метод в@Configuration-классе — когда объект создаёте вы сами, например это класс из чужой библиотеки, на который нельзя повесить аннотацию.
@Configuration
class AppConfig {
@Bean
ObjectMapper objectMapper() { // чужой класс — настраиваем руками
return new ObjectMapper().findAndRegisterModules();
}
}
Важная деталь: @Configuration оборачивается в прокси, поэтому вызов одного @Bean-метода из другого вернёт тот же синглтон, а не новый объект. Если же @Bean-методы лежат в обычном @Component, такой гарантии нет — зависимости туда передают через параметры метода.
Циклические зависимости
Если A требует B, а B требует A через конструктор — Spring не сможет их собрать и упадёт на старте (BeanCurrentlyInCreationException). С Spring Boot 2.6+ такое поведение включено по умолчанию. Это хорошо: цикл почти всегда означает, что обязанности размазаны и классы стоит разделить — например, вынести общую логику в третий бин. Обходить цикл через @Lazy или внедрение в поле — маскировка, а не решение.
Коротко
- IoC — принцип (созданием управляет контейнер), DI — приём реализации (зависимости передаются извне).
- Способов внедрения три (конструктор, сеттер, поле); выбирают конструктор —
final, видимость зависимостей, тестируемость. ApplicationContext— этоBeanFactoryплюс события, свойства, ресурсы, локализация.- Scope: singleton (по умолчанию), prototype, request, session, application, websocket.
- Singleton-бины делают stateless — один экземпляр обслуживает все потоки.
- prototype в singleton внедряют через
ObjectProvider, иначе экземпляр зафиксируется один раз. - Фазы бина: создание → внедрение →
@PostConstruct→ (прокси) → работа →@PreDestroy. - Циклическая зависимость через конструктор → падение на старте; это сигнал переразбить классы.
Что почитать дальше
- Жизненный цикл Spring-бина с примерами — каждая фаза с демо-кодом.
- Auto-configuration, properties, профили — как Spring Boot собирает контекст автоматически.
- Spring AOP — как
@Transactionalи подобное превращают класс в прокси.