Первый вопрос почти любого собеса по Spring: что такое IoC и DI и зачем они нужны. Дальше идут способы внедрения, разрешение неоднозначностей и циклические зависимости.
IoC и DI
Inversion of Control: объекты не создают свои зависимости сами, их создаёт и связывает контейнер. Dependency Injection это способ реализовать IoC: зависимости передаются объекту снаружи (через конструктор, сеттер или поле).
// без DI: класс сам решает, какую реализацию взять
class OrderService {
private final PaymentClient client = new HttpPaymentClient("https://pay");
}
// с DI: класс объявляет, что ему нужно
@Service
class OrderService {
private final PaymentClient client;
OrderService(PaymentClient client) { this.client = client; }
}
Выигрыш: слабая связанность, подмена реализации без правки кода, простые юнит-тесты (передал мок в конструктор).
Контейнер
BeanFactory базовый контейнер: создаёт бины и внедряет зависимости. ApplicationContext расширяет его: события, интернационализация, загрузка ресурсов, автоматическая регистрация BeanPostProcessor, по умолчанию создаёт singleton-бины сразу при старте. В приложениях используют ApplicationContext.
Откуда контейнер узнаёт о бинах:
@Componentи производные (@Service,@Repository,@Controller) плюс сканирование пакетов (@ComponentScan, его включает@SpringBootApplication);- методы
@Beanв классах@Configuration, когда класс не ваш или создание нетривиально; - XML (в новых проектах почти не встречается).
Способы внедрения
| Способ | Плюсы | Минусы |
|---|---|---|
| Конструктор | обязательные зависимости, final-поля, легко тестировать | длинный конструктор сразу показывает, что класс делает слишком много (это скорее плюс) |
| Сеттер | необязательные зависимости, перенастройка | объект может оказаться не до конца настроен |
Поле (@Autowired на поле) | коротко | нельзя final, в тестах нужен Spring или рефлексия, скрывает зависимости |
Рекомендация Spring: внедрение через конструктор. Если конструктор единственный, @Autowired на нём можно не писать (с Spring 4.3). С Lombok часто используют @RequiredArgsConstructor.
Когда кандидатов несколько
interface Notifier {}
@Component class EmailNotifier implements Notifier {}
@Component class SmsNotifier implements Notifier {}
@Service
class AlertService {
AlertService(Notifier notifier) {} // NoUniqueBeanDefinitionException
}
Варианты решения:
@Primaryна одной реализации: она выбирается по умолчанию;@Qualifier("smsNotifier")в точке внедрения (имеет приоритет над@Primary);- имя параметра совпадает с именем бина (запасной вариант, работает при сохранённых именах параметров);
List<Notifier>илиMap<String, Notifier>: внедряются все реализации, удобно для стратегий.
Optional<T>, ObjectProvider<T> и @Autowired(required = false) для необязательных зависимостей. ObjectProvider ещё и откладывает получение бина до момента вызова.
Циклические зависимости
A требует B, B требует A. С внедрением через конструкторы Spring не может создать ни один бин и падает с BeanCurrentlyInCreationException. Начиная с Spring Boot 2.6 циклы запрещены по умолчанию и для полей и сеттеров (spring.main.allow-circular-references=false).
Правильное решение не флаг и не @Lazy, а дизайн: вынести общую логику в третий бин, развернуть зависимость через событие или интерфейс. @Lazy на параметре конструктора подставляет прокси и откладывает создание, это допустимый временный обход.
Частые ошибки
- Создавать бин через
newи удивляться, что внутри не работают@Autowiredи@Transactional: объект не управляется контейнером. - Внедрение в поля и тесты, которые без Spring не запустить.
- Лечить цикл флагом
allow-circular-references.
Что спросят на собесе
- Что такое IoC и DI, чем они помогают?
- Какие способы внедрения есть и почему рекомендуют конструктор?
- Что будет, если подходящих бинов два? Как работают
@Primaryи@Qualifier? - Чем
BeanFactoryотличается отApplicationContext? - Как Spring ведёт себя с циклическими зависимостями и как их устранить?