IoC и внедрение зависимостей
Поднял контекст 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, остальные разбираются в тренажёре.
- Почему внедрение через конструктор предпочтительнее внедрения в поле (@Autowired на поле)?A)Внедрение в поле работает быстрее в рантайме, так как Spring не создаёт для него отдельный конструкторB)Зависимости обязательны и final, объект всегда валиден, легко тестировать без SpringC)Полевое внедрение — удобный способ обойти циклические зависимости между бинами в контейнереD)Конструкторное внедрение позволяет менять зависимости объекта в рантайме, а полевое — нет
показать ответ и разбор
+B)Зависимости обязательны и final, объект всегда валиден, легко тестировать без Spring// разбор: Конструкторное внедрение делает зависимости ОБЯЗАТЕЛЬНЫМИ и final: объект нельзя создать без них, он всегда в валидном состоянии, а неизменяемость безопасна для многопоточности. Такой класс легко тестировать БЕЗ Spring — просто new с моками в конструкторе. Ещё плюс: циклические зависимости всплывают сразу на старте (ошибка), а не прячутся. Полевое @Autowired скрывает зависимости, требует рефлексии/контейнера для теста и допускает частично сконструированные объекты. Рекомендация Spring — конструктор.
- Чем отличаются @Component, @Service, @Repository, @Controller?A)Это принципиально разные механизмы: только @Component создаёт бин, остальные регистрируют обычные классыB)@Service создаёт синглтон, @Component — прототип, @Repository — request-scope, @Controller — sessionC)Технически всё это @Component; остальные — специализации по роли/семантикеD)Они отличаются приоритетом внедрения: бины @Service внедряются раньше, чем бины @Component
показать ответ и разбор
+C)Технически всё это @Component; остальные — специализации по роли/семантике// разбор: Все четыре — стереотипные аннотации, помечающие класс как бин для component scan; @Service, @Repository, @Controller технически являются @Component с уточнённой РОЛЬЮ. Семантика: @Service — бизнес-логика, @Controller — веб-слой (обрабатывает запросы), @Repository — доступ к данным (плюс особое поведение: трансляция исключений персистентности в DataAccessException). Разница — в читаемости, слоистости и точечных возможностях, а не в самом факте создания бина. Скоуп по умолчанию у всех singleton.
- Что делает @Autowired при внедрении зависимости?A)Создаёт новый экземпляр зависимости через new при каждом обращении к аннотированному полюB)Помечает поле как транзакционное, оборачивая доступ к нему в отдельную транзакцию базы данныхC)Загружает значение поля из внешнего файла конфигурации по имени этого поля при старте приложенияD)Просит контейнер подставить подходящий бин (по типу, затем по имени)
показать ответ и разбор
+D)Просит контейнер подставить подходящий бин (по типу, затем по имени)// разбор: @Autowired указывает контейнеру внедрить сюда подходящий бин. Разрешение идёт СНАЧАЛА по типу; если бинов этого типа несколько — уточняют по имени поля/параметра, @Qualifier("name") или @Primary; если ни одного — ошибка (или необязательно при required=false/Optional). На конструкторе с одним аргументом @Autowired можно не писать (Spring внедрит автоматически). Это не создание объекта (new) и не чтение конфигурации (@Value) — это связывание уже существующих бинов контейнера.
- Что такое бин (bean) в Spring?A)Объект, созданием и жизненным циклом которого управляет контейнерB)Java-класс, помеченный ключевым словом bean в исходном коде на уровне объявления классаC)Специальный тип данных Spring для хранения конфигурации приложения в формате «ключ-значение»D)Единица развёртывания приложения — упакованный JAR-модуль, загружаемый контейнером сервлетов
показать ответ и разбор
+A)Объект, созданием и жизненным циклом которого управляет контейнер// разбор: Бин — это объект, которым УПРАВЛЯЕТ контейнер Spring (ApplicationContext): контейнер его создаёт, внедряет в него зависимости, вызывает lifecycle-колбэки и хранит согласно скоупу. Бины объявляют через стереотипы (@Component и производные, попадающие в component scan) или @Bean-методы в @Configuration. По умолчанию бины — синглтоны в пределах контейнера. Идея: своими объектами и их связями управляет контейнер, а не код вручную, — это и есть суть IoC-контейнера.
- Чем ApplicationContext богаче простого BeanFactory?A)ApplicationContext создаёт бины лениво, а BeanFactory — сразу все на старте, в этом всё различиеB)Даёт события, интернационализацию, AOP, авто-обработку аннотаций поверх DIC)BeanFactory умеет внедрять зависимости, а ApplicationContext — только хранить готовые объектыD)ApplicationContext работает лишь в веб-приложениях, тогда как BeanFactory — в консольных программах
показать ответ и разбор
+B)Даёт события, интернационализацию, AOP, авто-обработку аннотаций поверх DI// разбор: BeanFactory — базовый контейнер: создание бинов и внедрение зависимостей по запросу. ApplicationContext — его расширение с корпоративными возможностями: публикация/подписка событий (ApplicationEvent), интернационализация (MessageSource), удобный доступ к ресурсам, автоматическая обработка BeanPostProcessor'ов и аннотаций, интеграция с AOP. По умолчанию ApplicationContext инстанцирует синглтоны ЗАРАНЕЕ (eager) — ошибки конфигурации всплывают на старте. На практике почти всегда используют именно ApplicationContext.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.