JavaRush /Курсы /Spring Core /Подбор зависимостей Spring

Подбор зависимостей Spring

Spring Core
7 уровень , 3 лекция
Открыта

1. Резолвинг зависимостей и старт приложения

Пока мы писали конструкторы, всё выглядело романтично: «Я честно перечислил зависимости — значит, всё будет работать». Но контейнеру от вашей честности мало пользы, ему нужно конкретно понять, что подставлять в каждый параметр. Если вы не представляете этот процесс, любой stack trace на старте будет ощущаться как “Spring обиделся”. А он не обиделся — он просто не смог собрать объектный граф.

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

  • Точка внедрения (injection point) — место, куда нужно вставить зависимость. В рамках дня это чаще всего параметр конструктора, но в целом это может быть поле или метод.
  • Тип зависимости — класс или интерфейс параметра конструктора. Именно он является главным ключом поиска.
  • Кандидаты (candidates) — beans в контейнере, которые подходят по типу и могут быть внедрены.

Если держать эти три вещи в голове, половина «магии» испаряется сама по себе. Spring не выбирает «по настроению» — он делает понятный алгоритм: по типу, по кандидатам, и либо находит ровно одного, либо честно падает.

2. Когда Spring подставляет зависимости

Очень частая мысль новичка: «Я же ещё ничего не вызвал, почему приложение уже упало?». Потому что Spring не ждёт, пока вы начнёте создавать заказы; он сначала собирает ваше приложение как конструктор LEGO, а уже потом разрешает вам играть. Сборка происходит во время старта ApplicationContext, когда контейнер переходит от «я знаю, какие beans есть» к «я создаю реальные объекты».

Если сильно упростить (ровно настолько, чтобы не превращать лекцию в экскурсию по исходникам Spring), картина такая:

flowchart TD
    A[Старт ApplicationContext] --> B[Регистрация BeanDefinitions]
    B --> C[Создание singleton beans]
    C --> D[Выбор конструктора]
    D --> E[Резолвинг параметров по типу]
    E --> F[Создание объекта]
    F --> G["Инъекция полей/методов (если есть)"]
    G --> H[Bean готов к работе]

Нас сегодня интересует узкий, но самый “больной” участок: DE. В этот момент Spring смотрит на конструктор и пытается ответить на вопрос: «Какие конкретно beans подставить в параметры?».

И вот ключевое: если bean создаётся при старте контекста (обычно так и происходит для singleton-beans в учебном приложении), то ошибки резолвинга вы увидите на старте. Это и есть fail-fast поведение: лучше упасть сразу с понятным “нет кандидата”, чем через 20 минут выполнения сценария внезапно влететь в NullPointerException где-нибудь в середине отмены заказа.

3. Поиск кандидатов по типу

Когда у класса есть конструктор с параметрами, контейнер воспринимает эти параметры как список требований: “мне нужен bean такого-то типа”. И дальше он делает максимально приземлённую вещь: идёт в свой реестр и ищет beans, которые совместимы по типу.

Базовый «счастливый путь» выглядит так: для каждого параметра конструктора найден ровно один подходящий bean, значит можно собирать объект.

Посмотрим на максимально знакомый кусок ContextFlow: сервис аудита зависит от порта AuditWriter.

AuditWriter (порт):

public interface AuditWriter {
    // Контракт порта: сервисы будут писать аудит через этот метод
    void write(String message);
}

Реализация (инфраструктура), которая становится bean-ом:

import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Component;

@Component
public class ConsoleAuditWriter implements AuditWriter {
    public void write(String message) {
        // Здесь реальная "инфраструктура": пишем в stdout как в аудит-лог
        System.out.println("[AUDIT] " + message); // [AUDIT] created
    }
}

И сервис (application layer), которому нужен AuditWriter:

import com.example.contextflow.domain.ports.AuditWriter;
import org.springframework.stereotype.Service;

@Service
public class AuditService {
    private final AuditWriter auditWriter;

    public AuditService(AuditWriter auditWriter) {
        // Spring должен найти ровно один bean типа AuditWriter и подставить его сюда
        this.auditWriter = auditWriter;
    }
}

Почему это работает? Потому что в контейнере есть один bean, подходящий под тип AuditWriter: ConsoleAuditWriter. Spring видит параметр конструктора AuditWriter и находит кандидата по типу.

В виде псевдокода (чтобы мозг не утонул в терминах):

для каждого параметра конструктора:
  T = тип параметра
  candidates = все beans, которые "являются T"
  если candidates.size == 1 -> берём его
  если candidates.size == 0 -> ошибка: кандидата нет
  если candidates.size > 1 -> ошибка: кандидатов несколько

Отдельно важно: тот же механизм действует не только для конструкторов, но и для параметров @Bean-методов. То есть Spring одинаково “доставляет” зависимости и сюда:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ScenarioConfig {
    @Bean
    public ScenarioRunner scenarioRunner(AuditService auditService) {
        // Зависимость AuditService тоже резолвится контейнером по типу параметра
        return new ScenarioRunner(auditService);
    }
}

Идея одна: тип параметра → нужен bean этого типа.

4. Ошибка: кандидата нет

Теперь самое жизненное: вы написали конструктор, всё красиво, но контейнер говорит “нет”. Это случается, когда по нужному типу не найдено ни одного кандидата. И тогда Spring не молчит, а падает со связкой исключений, которая сначала выглядит страшно, а потом — как очень полезная подсказка.

Смоделируем типовую ситуацию. Представьте, что вы забыли сделать реализацию AuditWriter bean-ом: убрали @Component или случайно вынесли класс в пакет, который не попадает в @ComponentScan.

Вот «плохая» версия (аннотацию забыли):

import com.example.contextflow.domain.ports.AuditWriter;

// @Component  <-- забыли, и класс не стал bean-ом
public class ConsoleAuditWriter implements AuditWriter {
    public void write(String message) {
        // Класс не зарегистрирован как bean -> для типа AuditWriter кандидатов в контейнере не будет
        System.out.println("[AUDIT] " + message);
    }
}

Что произойдёт при старте контекста? Контейнер попытается создать AuditService, увидит конструктор AuditService(AuditWriter auditWriter) и пойдёт искать bean типа AuditWriter. И не найдёт ничего.

Фрагмент сообщения об ошибке (упрощённый, но по смыслу максимально близкий):

UnsatisfiedDependencyException: Error creating bean 'auditService'
...
Caused by: NoSuchBeanDefinitionException:
No qualifying bean of type '...AuditWriter' available

Как это читать по-человечески? Начните не с верхней строчки, а с сути:

Контейнер пытался создать bean auditService, но не смог удовлетворить зависимость: ему нужен AuditWriter, а такого bean-а нет.

Чтобы это закрепилось, вот маленькая табличка (вместо бесконечных списков исключений):

Что вы сделали в коде Что увидел Spring Чем это закончилось
В конструкторе есть параметр AuditWriter “Мне нужен bean типа AuditWriter Если в контейнере нет кандидатов — NoSuchBeanDefinitionException
Вы забыли @Component/@Bean на реализации “Кандидатов 0” UnsatisfiedDependencyException с причиной NoSuchBeanDefinitionException

Важно понять момент времени: ошибка возникает не когда вы вызываете методы AuditService, а когда Spring пытается создать AuditService как bean. То есть это ошибка уровня сборки приложения.

В ContextFlow такие проблемы чаще всего происходят из-за трёх “приземлённых” причин: вы забыли аннотацию на классе, вы забыли @Bean в конфигурации, или вы сузили границы @ComponentScan так, что класс больше не сканируется. Всё остальное — редкие случаи, и мы пока туда не уходим.

5. Ошибка: кандидатов слишком много

Вторая популярная боль — противоположная: bean-ы есть, даже слишком есть. И тогда новичок обычно спрашивает: «Ну пусть Spring выберет любой…». Spring на это отвечает очень взрослым способом: “нет”. Потому что “любой” сегодня может оказаться SmsNotificationSender, а завтра — ConsoleNotificationSender, и вы получите баг, который меняется от порядка загрузки классов. Такие баги особенно любят появляться в пятницу вечером.

Покажем ситуацию на уведомлениях. Есть интерфейс:

public interface NotificationSender {
    // Отправка уведомления: конкретный канал (консоль/SMS/почта) решает реализация
    void send(String message);
}

И две реализации, которые обе стали beans:

import org.springframework.stereotype.Component;

@Component
public class ConsoleNotificationSender implements NotificationSender {
    public void send(String message) {
        // Реализация-канал: просто печатаем в консоль
        System.out.println("[CONSOLE] " + message);
    }
}
import org.springframework.stereotype.Component;

@Component
public class SmsNotificationSender implements NotificationSender {
    public void send(String message) {
        // Реализация-канал: имитируем отправку по SMS
        System.out.println("[SMS] " + message);
    }
}

А сервис ожидает один NotificationSender:

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    public NotificationDispatchService(NotificationSender sender) {
        // Если кандидатов NotificationSender больше одного, контейнер не сможет выбрать автоматически
    }
}

Контейнер видит параметр типа NotificationSender, собирает кандидатов и обнаруживает: “их два”. Итог — ошибка, смысл которой обычно такой:

NoUniqueBeanDefinitionException:
expected single matching bean but found 2

Заметьте, это не «ошибка Spring». Это контейнер честно сообщает: “ты попросил один объект типа NotificationSender, а я вижу два, и не имею права угадывать”.

И здесь снова важно “когда”: ошибка всплывает в момент создания NotificationDispatchService, потому что именно тогда Spring резолвит параметры конструктора.

В рамках сегодняшней лекции мы не превращаем это в «как выбрать правильного кандидата» (это отдельная дисциплина), но фиксируем главное: для одной точки внедрения нужен один однозначный кандидат, иначе контейнер останавливает старт. Это защитная реакция, а не каприз.

И здесь возникает естественный следующий вопрос: как жить, если реализаций интерфейса несколько и выбрать одну всё-таки надо? Пока нам достаточно зафиксировать базовую модель — контейнер ищет по типу и требует одного однозначного кандидата. Поэтому две стартовые поломки дня выглядят так просто: кандидата нет или кандидатов слишком много.

6. Важные детали резолвинга

На практике почти все проблемы резолвинга решаются пониманием двух вещей: “по какому типу я ищу” и “сколько кандидатов реально зарегистрировано”. Но иногда Spring пытается помочь вам тонкими подсказками, и полезно знать, где они живут, чтобы не удивляться странному поведению. Самая полезная из таких подсказок — имена параметров конструктора, а ещё одна — обобщённые типы (generics).

Имена параметров и флаг -parameters

В baseline курса у нас включён компиляторный флаг -parameters. Он делает простую, но важную вещь: сохраняет в байткоде реальные имена параметров конструктора. Без него рефлексия часто видит не auditWriter, а что-то вроде arg0, arg1.

Зачем это контейнеру? Иногда, если кандидатов несколько, Spring может посмотреть на имя параметра как на подсказку. Например, если у вас есть bean с именем smsSender, и параметр конструктора тоже называется smsSender, контейнер может попытаться сопоставить их.

Пример (показываю как механику, а не как “идеальный стиль на века”):

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    public NotificationDispatchService(NotificationSender smsSender) {
        // имя параметра: smsSender
    }
}

Если один из NotificationSender-beans действительно называется smsSender, это может стать tiebreaker-ом. Но тут есть подвох: переименовали параметр — сломали wiring. Поэтому воспринимайте это как “полезно знать, почему что-то вдруг заработало/сломалось”, а не как главный способ проектирования.

Generics как часть типа

Хотя Java стирает generic-типы во время выполнения (type erasure), Spring умеет читать generics из метаданных классов и использовать их при резолвинге. Это встречается реже на старте пути, но иногда спасает от неоднозначности.

Идея на уровне примера: у вас есть интерфейс Formatter<T> и два бина — один для DailyReport, другой для AuditRecord. Тогда зависимость Formatter<DailyReport> может быть однозначной даже при двух реализациях Formatter.

Короткий скелет точки внедрения:

import org.springframework.stereotype.Service;

@Service
public class ReportingService {
    public ReportingService(Formatter<DailyReport> formatter) {
    }
}

Суть здесь не в том, чтобы срочно тащить generics в ContextFlow (не надо), а в том, чтобы понимать: для Spring “тип” иногда чуть богаче, чем просто Formatter. Это объясняет, почему в некоторых проектах люди спокойно живут с несколькими реализациями одного generic-интерфейса и не ловят NoUniqueBeanDefinitionException.

Быстрый поиск причины ошибки

И наконец — навык, который экономит часы: смотреть на корневую причину. В типичной цепочке исключений верхняя часть говорит “BeanCreationException/UnsatisfiedDependencyException”, а полезное — ниже:

  • если внизу NoSuchBeanDefinitionException, значит кандидатов 0;
  • если внизу NoUniqueBeanDefinitionException, значит кандидатов > 1;
  • если в тексте есть конкретный тип параметра конструктора — это ваш “ключ поиска”.

Очень часто достаточно одной мысли: “какой именно тип он ищет?” — и вы мгновенно находите проблему в коде или в конфигурации.

7. Типичные ошибки при резолвинге

Эта часть нужна не для того, чтобы вас напугать, а чтобы вы узнали себя в паре ситуаций и быстрее выбрались. Ошибки резолвинга зависимостей почти всегда повторяются: меняется только имя интерфейса и названия сервисов. Если научиться узнавать паттерн “0 кандидатов / 2 кандидата”, Spring перестаёт быть страшным и становится довольно прямолинейным инструментом.

Ошибка №1: “Я создал интерфейс и забыл про реализацию как bean”.
Часто это выглядит так: в конструкторе вы честно просите AuditWriter, а реализацию ConsoleAuditWriter забыли пометить @Component или забыли зарегистрировать через @Bean. В результате контейнер видит красивый контракт и пустоту в реестре. Лечится не философией, а банальной проверкой: реализация действительно попадает под @ComponentScan или реально создаётся методом @Bean.

Ошибка №2: “Я сузил @ComponentScan, чтобы было ‘чище’, и случайно вырубил половину приложения”.
Когда границы сканирования становятся слишком узкими, вы получаете эффект “всё компилируется, но контекст падает”. Новичку кажется, что сломался бизнес-код, но на самом деле контейнер просто не зарегистрировал нужный класс как bean. В такой ситуации полезно на минуту забыть про домен и спросить себя: “этот пакет вообще сканируется?”.

Ошибка №3: “Появилось две реализации — и я ожидал, что Spring выберет любую”.
Это классический конфликт: вы добавили второй NotificationSender, а сервис по-прежнему просит один NotificationSender. Контейнер отказывается угадывать и падает с NoUniqueBeanDefinitionException. Здесь важно не злиться на Spring, а понять, что он защищает вас от случайного поведения. Если выбор должен быть однозначным, он должен быть однозначным уже на уровне конфигурации, а не “как получится”.

Ошибка №4: “Я читаю только верхнюю строчку stack trace”.
Верхняя строчка почти всегда говорит “не могу создать bean X”, и это редко является настоящей причиной. Настоящая причина обычно в самом низу: либо отсутствующий тип (NoSuchBeanDefinitionException), либо неоднозначность (NoUniqueBeanDefinitionException). Если начать читать снизу вверх, внезапно выясняется, что Spring пишет достаточно человеческие вещи — просто очень многословно.

Ошибка №5: “Я надеюсь на имена параметров как на основной механизм выбора”.
Иногда всё “магически” заработало после переименования параметра конструктора, и хочется продолжать в том же духе. Проблема в том, что это хрупко: переименовали параметр при рефакторинге — и wiring поменялся или развалился. Имена параметров полезны как подсказка и как часть диагностики (особенно при включённом -parameters), но опираться на них как на фундамент дизайна — рискованно.

1
Задача
Spring Core, 7 уровень, 3 лекция
Недоступна
Ошибка отсутствующего кандидата
Ошибка отсутствующего кандидата
1
Задача
Spring Core, 7 уровень, 3 лекция
Недоступна
Ошибка неоднозначного выбора по типу
Ошибка неоднозначного выбора по типу
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ