Логотип Workflow

Article

Ioc And Di Container

Этап 2 — Spring Core: сначала Bean, потом IoC и DI

Что такое Bean в Spring

Bean — это объект, который создан, зарегистрирован и управляется контейнером ApplicationContext.

ApplicationContext управляет именованными Bean-ами

В обычном Java-коде вы создаёте объект через new и сами отвечаете за его жизненный цикл.

class PaymentClient {
    void charge(String orderId) {
        System.out.println("Charging order " + orderId);
    }
}

class OrderService {
    private final PaymentClient paymentClient;

    OrderService() {
        this.paymentClient = new PaymentClient();
    }
}

Этот код простой, но теперь OrderService сам решает, как создать PaymentClient. Если позже понадобится тестовая версия, sandbox-версия, логирование, метрики или транзакционное поведение вокруг зависимости, код создания объектов придётся менять вручную. Spring меняет модель ответственности: вы описываете, какие классы должны стать Bean-ами и какие зависимости им нужны, а контейнер создаёт runtime-граф объектов.

В Spring, как только класс становится Bean, он становится частью приложения, управляемого контейнером. Его поведение зависит не только от кода класса, но и от конфигурации, активных профилей, post-processors, lifecycle callbacks и иногда proxy. Поэтому Bean — это не «любой Java-объект». Это объект, о котором знает контейнер.

ПонятиеОбычный Java-объектSpring Bean
СозданиеОбычно new в вашем кодеСоздаётся из bean definitions
ЗависимостиПередаются вручнуюВнедряются контейнером
LifecycleКонтролируется вызывающим кодомУправляется ApplicationContext
ИнфраструктураНет автоматического Spring-поведенияВозможны proxy, callbacks, profiles

Где берутся Bean-ы

Контейнер получает определения бинов из двух основных источников: component scanning (@Component, @Service, @Repository, @Controller) и Java-конфигурация через @Configuration + @Bean. Сканирование удобно для application-кода, потому что Spring находит аннотированные классы в настроенных пакетах. @Bean полезен для внешних библиотек или когда правила создания нужно контролировать явно.

Ключевое отличие: если объект создан вручную через new в случайном методе, контейнер не начинает автоматически им управлять. Например, @Transactional, @Async, validation proxies и lifecycle callbacks применяются к Spring-managed Bean-ам, а не к произвольным объектам, которые вы создали сами.

IoC (Inversion of Control)

IoC означает Inversion of Control, то есть «инверсия управления». Формулировка звучит абстрактно, но идея простая: меняется место, которое контролирует создание объектов и связывание зависимостей. В ручном Java-коде классы часто сами решают, какие collaborators создать и как их соединить. В Spring этим этапом композиции владеет контейнер. Ваш код объявляет потребности, а контейнер эти потребности закрывает.

Без IoC OrderService мог бы создавать PaymentClient, InventoryClient и OrderRepository прямо внутри конструктора. Тогда сервис одновременно отвечает за бизнес-логику и за сборку object graph. С IoC OrderService говорит: «мне нужны эти зависимости». Spring выбирает конкретные Bean-ы, создаёт их, внедряет и держит доступными во время работы приложения.

Это важно, потому что большие приложения постоянно меняются. Сегодня используется настоящий payment client, завтра нужна sandbox-версия в dev, mock в тестах и metrics wrapper в production. Если каждый класс сам создаёт зависимости, изменения расползаются по многим файлам. Если композицией владеет контейнер, многие изменения становятся решением на уровне конфигурации.

DI (Dependency Injection)

DI означает Dependency Injection, то есть «внедрение зависимостей». Это основной способ, которым Spring реализует IoC. Зависимость — это другой объект, который нужен классу для работы. Внедрение означает, что зависимость приходит извне, а не создаётся внутри класса.

Constructor injection — базовый вариант для важного application-кода:

@Service
class OrderService {
    private final PaymentClient paymentClient;
    private final OrderRepository orderRepository;

    OrderService(PaymentClient paymentClient, OrderRepository orderRepository) {
        this.paymentClient = paymentClient;
        this.orderRepository = orderRepository;
    }
}

Здесь OrderService честно показывает свои требования. Он не может существовать без PaymentClient и OrderRepository, поэтому зависимости находятся в конструкторе. Spring видит конструктор, находит подходящие Bean-ы и передаёт их внутрь. Такой класс легче тестировать: unit-тест может напрямую передать fake-зависимости. Его легче читать: контракт класса виден сразу.

Field injection часто кажется быстрее, но скрывает контракт класса и усложняет тестирование. Если зависимость обязательна для корректного состояния объекта, constructor injection выражает это требование явно.

Жизненный цикл Bean и постобработка

Жизненный цикл Bean — это не просто «создали и используем». Spring читает bean definitions, создаёт объекты, внедряет зависимости, вызывает initialization callbacks, применяет post-processors, делает Bean доступным, а при закрытии контекста вызывает destroy callbacks.

Жизненный цикл Bean

Особенно важна постобработка. Spring может обернуть Bean в proxy для транзакций, безопасности, кеширования, observability или AOP. Поэтому runtime-поведение может отличаться от «голой» реализации класса. Когда вы вызываете метод proxy-Bean-а, перед целевым методом может выполниться инфраструктурный код.

Это объясняет многие проблемы в отладке. Если @Transactional не работает, причина часто не в SQL-коде, а в том, что объект создан вручную, метод вызван изнутри того же класса или вызов не проходит через Spring proxy.

Scope: почему singleton не всегда один

singleton в Spring означает «один экземпляр на контейнер», а не «один на всю JVM». Если в приложении несколько application contexts, у каждого может быть свой singleton. Это важно в тестах, модульных приложениях и системах с дочерними контекстами. Scope описывает, как контейнер переиспользует экземпляры Bean-ов, а не универсальное правило Java.

ApplicationContext и BeanFactory

BeanFactory — минимальная абстракция DI-контейнера. ApplicationContext — полноценный runtime-контейнер с событиями, профилями, i18n, загрузкой ресурсов, доступом к environment и инфраструктурой post-processing. В Spring Boot приложениях вы фактически работаете с ApplicationContext, даже если почти никогда не обращаетесь к нему напрямую.

Что происходит на старте приложения

На старте Spring читает конфигурацию и classpath, строит bean definitions, разрешает зависимости, создаёт singleton Bean-ы, применяет post-processors, выполняет initialization callbacks и публикует готовый контекст. Важно понимать причинно-следственную связь: ошибки старта почти всегда относятся к одному из этих шагов. Missing Bean означает, что зависимость не удалось разрешить. Circular dependency означает, что граф нельзя чисто построить. Ошибка init-метода означает, что объект создан, но не смог стать готовым.

Практический пример

Представь сервис оформления заказа. Вручную ты создаёшь OrderService, затем отдельно создаёшь PaymentClient, InventoryClient, OrderRepository и передаёшь их по цепочке в конструкторы. В Spring та же композиция задаётся через Bean-модель: контейнер читает определения, строит граф, внедряет зависимости и гарантирует согласованный lifecycle. Разница особенно заметна, когда нужно заменить PaymentClient на sandbox-версию в dev-профиле: при ручной сборке это каскад правок, в контейнерной модели — изменение конфигурации/бин-выбора.

Граф Bean-ов сервиса заказа

Граф важен потому, что OrderService должен координировать бизнес-шаги, а не решать стратегию создания каждого collaborator. Если inventory переедет в другой API, контракт конструктора сервиса может остаться тем же, а поменяется только реализация InventoryClient. Если изменится persistence, OrderRepository можно заменить за той же dependency boundary. IoC и DI делают такие точки замены явными.

Итог этапа

Если ты чётко понимаешь Bean-механику, IoC и DI перестают быть терминами из собеседования и становятся рабочей моделью. Bean означает «объект под управлением контейнера». IoC означает «контейнер управляет композицией». DI означает «зависимости приходят извне». Вместе они позволяют Spring строить и поддерживать согласованный runtime-граф вместо того, чтобы каждый класс вручную собирал приложение.

Нюансы и типичные ошибки

  • Создавать объекты через new внутри сервисов и ожидать, что к ним применится @Transactional или @Async.
  • Использовать field injection в критичных компонентах и терять явность контрактов.
  • Держать изменяемое состояние в singleton Bean без понимания конкурентного доступа.
  • Неправильно располагать пакеты, из-за чего component scan «не видит» часть приложения.
  • Использовать ApplicationContext как service locator повсюду вместо объявления зависимостей в конструкторах.

Чек-лист понимания

  • Могу объяснить, чем Bean отличается от обычного Java-объекта.
  • Могу описать полный lifecycle Bean от definition до destroy.
  • Понимаю, почему constructor injection считается базовой практикой.
  • Понимаю, почему IoC выносит сборку object graph из бизнес-классов.
  • Понимаю, почему singleton ограничен контейнером, а не всей JVM.

Quiz

Проверьте, что вы усвоили

Авторизуйтесь чтоб пройти тесты