← назад к разделу

Это первая тема в 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(); // новый экземпляр каждый раз
    }
}

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

От создания до остановки бин проходит несколько фаз:

  1. создание (вызов конструктора);
  2. внедрение зависимостей;
  3. колбэки инициализации@PostConstruct, затем afterPropertiesSet();
  4. обёртывание в прокси (если нужно — например, для @Transactional);
  5. бин готов к работе;
  6. при остановке приложения — @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 и подобное превращают класс в прокси.