сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Spring и Hibernate

IoC и внедрение зависимостей

IoC и DI: кто создаёт объекты вместо тебя

Поднял контекст Spring - так называют запущенный контейнер со всеми собранными объектами - и позвал метод сервиса двумя способами. Через объект, который выдал контейнер, транзакция открылась и закоммитилась. Через new Orders() в логе пусто: ни открытия, ни коммита, метод просто выполнился.

Разница в том, что объект из контейнера обёрнут прокси - невидимой обёрткой, которая вклинивается перед вызовом метода, - а созданный руками не обёрнут ничем. Отсюда половина вопросов «почему аннотация не сработала». На собесе это самая база: способы внедрения, почему один считают дефолтом, и что происходит при конфликте бинов.

// Формулировки: «чем IoC отличается от DI?», «какие виды внедрения знаешь?», «почему ругают внедрение в поле?», «что будет при двух бинах одного типа?».

Контейнер, бин и три способа внедрения

Бин - это объект, созданием и жизнью которого управляет Spring, а не твой код. Контейнер читает конфигурацию, строит граф таких объектов и раздаёт их друг другу. Отсюда и название: IoC (inversion of control, инверсия управления) - принцип «не ты создаёшь зависимости», а DI (dependency injection, внедрение зависимостей) - конкретный способ его реализовать, когда зависимости приходят снаружи.

Способов внедрения три. Через конструктор - зависимости приходят аргументами: без них объект не собрать, поля можно объявить final. Через сеттер - для необязательных. Через поле с @Autowired - контейнер проставляет значение рефлексией, то есть напрямую в поле, минуя конструктор.

// Практическая разница вылезает в первом же тесте. Класс с внедрением через конструктор собирается обычным new с подставными объектами. Класс с внедрением в поле без контейнера получит null - тест придётся тащить через Spring либо лезть рефлексией.

бин
объект, созданием и жизнью которого управляет Spring
IoC / DI
принцип «создаёшь не ты» / внедрение зависимостей как его реализация

Откуда контейнер узнаёт о бинах и что делает при конфликте

Объявить бин можно двумя путями. Пометить свой класс стереотипом - @Component или его уточнениями @Service, @Repository, @Controller - и включить сканирование пакетов. Либо описать сборку руками: класс с @Configuration и методы с @Bean, это способ для чужих классов из библиотек.

Когда бинов одного типа оказывается два, контейнер не угадывает. Я специально завёл два и получил при старте: NoUniqueBeanDefinitionException: No qualifying bean of type 'Clock' available: expected single matching bean but found 2: autoClock,myClock. В сообщении перечислены оба имени - дальше либо @Primary на том, что должен быть по умолчанию, либо @Qualifier с именем нужного в месте внедрения.

Зеркальная ошибка - NoSuchBeanDefinitionException, «не нашёл ни одного». Она же прилетает в неожиданном месте: если бин реализует интерфейс, Spring оборачивает его прокси по интерфейсу, и такой прокси НЕ является экземпляром исходного класса. Я проверил - запрос бина по конкретному классу упал с этой ошибкой, хотя по интерфейсу выдавался нормально. Отсюда правило: внедряй по интерфейсу.

@Component / @Bean
бин сканированием своего класса / фабричным методом в @Configuration
@Primary / @Qualifier
кандидат по умолчанию / выбор конкретного по имени

Циклические зависимости

Свёл два бина в кольцо: A требует B в конструкторе, B требует A. Контекст не поднялся - BeanCurrentlyInCreationException. Причина простая: чтобы создать A, нужен готовый B, а чтобы создать B, нужен готовый A. Выхода из этого круга нет.

Тот же цикл, но через поля, в голом Spring поднимается: контейнер сперва создаёт оба объекта пустыми, потом расставляет ссылки. А вот Spring Boot такое запрещает по умолчанию и печатает схему цикла со стрелками плюс текст: «Relying upon circular references is discouraged and they are prohibited by default», и подсказку про spring.main.allow-circular-references.

Подсказку эту лучше не использовать. Цикл означает, что два класса знают друг о друге слишком много; правильное лечение - вынести общую часть в третий бин. Флаг и @Lazy только откладывают создание и оставляют проблему дизайна на месте.

// цикл через конструкторы - контекст не стартует:
// BeanCurrentlyInCreationException

// цикл через поля под Spring Boot:
// ┌─────┐
// |  a (field B A.b)
// ↑     ↓
// |  b (field A B.a)
// └─────┘
// Relying upon circular references is discouraged
// and they are prohibited by default.
циклическая зависимость
два бина требуют друг друга: старт падает, дизайн под вопросом

Как отвечать: «Почему внедрение через конструктор считают дефолтом?»

Три довода. Первый - честная обязательность: зависимость приходит аргументом, объект нельзя создать наполовину, а поле можно объявить final и получить неизменяемость, а с ней потокобезопасность. Второй - тестируемость: класс собирается обычным new с подставными объектами, без поднятия контекста; при внедрении в поле без контейнера там будет null, и тест придётся тащить через Spring или рефлексию. Третий довод самый интересный: конструктор с шестью аргументами выглядит громоздко, и это честный сигнал, что класс делает слишком много, тогда как внедрение в поле ту же перегруженность прячет. Ещё я всегда внедряю по интерфейсу, а не по классу: если бин реализует интерфейс, Spring оборачивает его прокси, и запрос по конкретному классу падает с NoSuchBeanDefinitionException - я на это натыкался.

Почему это сильный ответ: три независимых довода, причём последний переворачивает кажущийся недостаток конструктора в достоинство, плюс практическая деталь про прокси и интерфейс.

На чём валят

  • Внедрение в поле повсюду. Юнит-тест без контейнера получает null, а разрастание зависимостей перестаёт быть заметным.
  • Создать объект через new и ждать работы аннотаций. У меня такой объект отработал вообще без транзакции: прокси нет - магии нет.
  • Два бина одного типа без @Qualifier. Старт падает, но в сообщении перечислены оба имени - его надо прочитать, а не перебирать варианты.
  • Внедрять по конкретному классу, когда у бина есть интерфейс. Прокси реализует интерфейс и экземпляром класса не является.
  • Чинить цикл флагом allow-circular-references или @Lazy. Старт починится, кольцо в дизайне останется.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 17, остальные разбираются в тренажёре.

  1. #ioc_di1 / 5
    Почему внедрение через конструктор предпочтительнее внедрения в поле (@Autowired на поле)?
    A)Внедрение в поле работает быстрее в рантайме, так как Spring не создаёт для него отдельный конструктор
    B)Зависимости обязательны и final, объект всегда валиден, легко тестировать без Spring
    C)Полевое внедрение — удобный способ обойти циклические зависимости между бинами в контейнере
    D)Конструкторное внедрение позволяет менять зависимости объекта в рантайме, а полевое — нет
    показать ответ и разбор
    +B)Зависимости обязательны и final, объект всегда валиден, легко тестировать без Spring

    // разбор: Конструкторное внедрение делает зависимости ОБЯЗАТЕЛЬНЫМИ и final: объект нельзя создать без них, он всегда в валидном состоянии, а неизменяемость безопасна для многопоточности. Такой класс легко тестировать БЕЗ Spring — просто new с моками в конструкторе. Ещё плюс: циклические зависимости всплывают сразу на старте (ошибка), а не прячутся. Полевое @Autowired скрывает зависимости, требует рефлексии/контейнера для теста и допускает частично сконструированные объекты. Рекомендация Spring — конструктор.

  2. #ioc_di2 / 5
    Чем отличаются @Component, @Service, @Repository, @Controller?
    A)Это принципиально разные механизмы: только @Component создаёт бин, остальные регистрируют обычные классы
    B)@Service создаёт синглтон, @Component — прототип, @Repository — request-scope, @Controller — session
    C)Технически всё это @Component; остальные — специализации по роли/семантике
    D)Они отличаются приоритетом внедрения: бины @Service внедряются раньше, чем бины @Component
    показать ответ и разбор
    +C)Технически всё это @Component; остальные — специализации по роли/семантике

    // разбор: Все четыре — стереотипные аннотации, помечающие класс как бин для component scan; @Service, @Repository, @Controller технически являются @Component с уточнённой РОЛЬЮ. Семантика: @Service — бизнес-логика, @Controller — веб-слой (обрабатывает запросы), @Repository — доступ к данным (плюс особое поведение: трансляция исключений персистентности в DataAccessException). Разница — в читаемости, слоистости и точечных возможностях, а не в самом факте создания бина. Скоуп по умолчанию у всех singleton.

  3. #ioc_di3 / 5
    Что делает @Autowired при внедрении зависимости?
    A)Создаёт новый экземпляр зависимости через new при каждом обращении к аннотированному полю
    B)Помечает поле как транзакционное, оборачивая доступ к нему в отдельную транзакцию базы данных
    C)Загружает значение поля из внешнего файла конфигурации по имени этого поля при старте приложения
    D)Просит контейнер подставить подходящий бин (по типу, затем по имени)
    показать ответ и разбор
    +D)Просит контейнер подставить подходящий бин (по типу, затем по имени)

    // разбор: @Autowired указывает контейнеру внедрить сюда подходящий бин. Разрешение идёт СНАЧАЛА по типу; если бинов этого типа несколько — уточняют по имени поля/параметра, @Qualifier("name") или @Primary; если ни одного — ошибка (или необязательно при required=false/Optional). На конструкторе с одним аргументом @Autowired можно не писать (Spring внедрит автоматически). Это не создание объекта (new) и не чтение конфигурации (@Value) — это связывание уже существующих бинов контейнера.

  4. #ioc_di4 / 5
    Что такое бин (bean) в Spring?
    A)Объект, созданием и жизненным циклом которого управляет контейнер
    B)Java-класс, помеченный ключевым словом bean в исходном коде на уровне объявления класса
    C)Специальный тип данных Spring для хранения конфигурации приложения в формате «ключ-значение»
    D)Единица развёртывания приложения — упакованный JAR-модуль, загружаемый контейнером сервлетов
    показать ответ и разбор
    +A)Объект, созданием и жизненным циклом которого управляет контейнер

    // разбор: Бин — это объект, которым УПРАВЛЯЕТ контейнер Spring (ApplicationContext): контейнер его создаёт, внедряет в него зависимости, вызывает lifecycle-колбэки и хранит согласно скоупу. Бины объявляют через стереотипы (@Component и производные, попадающие в component scan) или @Bean-методы в @Configuration. По умолчанию бины — синглтоны в пределах контейнера. Идея: своими объектами и их связями управляет контейнер, а не код вручную, — это и есть суть IoC-контейнера.

  5. #ioc_di5 / 5
    Чем ApplicationContext богаче простого BeanFactory?
    A)ApplicationContext создаёт бины лениво, а BeanFactory — сразу все на старте, в этом всё различие
    B)Даёт события, интернационализацию, AOP, авто-обработку аннотаций поверх DI
    C)BeanFactory умеет внедрять зависимости, а ApplicationContext — только хранить готовые объекты
    D)ApplicationContext работает лишь в веб-приложениях, тогда как BeanFactory — в консольных программах
    показать ответ и разбор
    +B)Даёт события, интернационализацию, AOP, авто-обработку аннотаций поверх DI

    // разбор: BeanFactory — базовый контейнер: создание бинов и внедрение зависимостей по запросу. ApplicationContext — его расширение с корпоративными возможностями: публикация/подписка событий (ApplicationEvent), интернационализация (MessageSource), удобный доступ к ресурсам, автоматическая обработка BeanPostProcessor'ов и аннотаций, интеграция с AOP. По умолчанию ApplicationContext инстанцирует синглтоны ЗАРАНЕЕ (eager) — ошибки конфигурации всплывают на старте. На практике почти всегда используют именно ApplicationContext.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.