JavaRush /Курсы /Spring Core /Default bean: @Primary

Default bean: @Primary и @Fallback

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

1. Конфликт кандидатов и выбор по умолчанию

Когда вы впервые ловите NoUniqueBeanDefinitionException, возникает очень человеческое желание: “Да выбери уже хоть что-нибудь, мне надо запустить проект, я потом разберусь”. Это чувство знакомо каждому разработчику, особенно в понедельник утром. Но именно в этот момент Spring делает нам подарок: он заставляет сформулировать договорённость — что считать основным вариантом, а что считать запасным.

Представьте, что у вас есть интерфейс NotificationSender, и вы добавили две реализации: EmailNotificationSender и SmsNotificationSender. Обе честно зарегистрированы как beans. Контейнер видит: “подходит по типу — подходит”. Но когда сервис просит один NotificationSender, у контейнера нет права выбрать случайно: иначе поведение приложения будет зависеть от порядка сканирования классов, внутренних деталей сборки и фазы Луны (в проде — особенно любит полнолуние).

Снова возьмём намеренно простой one-sender вариант NotificationDispatchService: он нужен только как чистая точка внедрения, на которой видно поведение default-choice.

Вот минимальная иллюстрация “больного места” — сервис не делает ничего подозрительного, он просто хочет один объект:

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        // Spring внедряет сюда один bean типа NotificationSender.
        // Если кандидатов несколько и нет правил выбора — будет NoUniqueBeanDefinitionException.
        this.sender = sender;
    }
}

Если NotificationSender зарегистрирован в контейнере в двух экземплярах, Spring обязан сказать: “Окей, но какой именно?”. И вот тут как раз появляются два инструмента, которые мы сегодня разберём: @Primary и @Fallback. Они помогают описать “обычный” выбор и “резервный” вариант на уровне контейнера, а не через if/else внутри бизнес‑методов.

2. @Primary: основной вариант

С @Primary мы решаем очень бытовую задачу: “В большинстве мест приложения мне нужен вот этот вариант, без дополнительных уточнений”. Это похоже на настройку “браузер по умолчанию” в операционной системе. У вас может быть установлено пять браузеров, но если вы просто кликаете по ссылке, система должна выбрать один — основной. @Primary — это такая же настройка, только для Spring‑контейнера.

Смысл простой: если по типу подходит несколько beans, но нужен один, и ровно один из них помечен @Primary, то Spring выберет именно его. При этом остальные beans никуда не исчезают: они остаются в контейнере и могут использоваться там, где выбор делается явно или где сервису нужен весь набор реализаций.

Показать @Primary можно двумя естественными способами: на классе‑компоненте или на @Bean-методе в конфигурации. По смыслу это одно и то же — пометка попадает в bean definition.

2.1. @Primary на компоненте

В нашем проекте ContextFlow логично сделать “самый безопасный” канал уведомлений основным — консоль. Для учебного non-web приложения это прям идеальный default: всегда работает, не требует внешней инфраструктуры и не заставляет студента настраивать SMTP в середине дня.

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Primary // Этот bean будет выбран по умолчанию, если кандидатов несколько
@Component
public class ConsoleNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        // Самый "безопасный" канал: всегда доступен, не требует инфраструктуры
        System.out.println("CONSOLE: " + message); // CONSOLE: Order created
    }
}

Теперь если у вас есть ещё EmailNotificationSender и SmsNotificationSender, и какой‑то сервис просит просто NotificationSender, контейнер выберет ConsoleNotificationSender, потому что он “главный”.

2.2. @Primary на @Bean-методе

Иногда реализацию удобнее держать не как отдельный @Component, а как @Bean (например, потому что вам нужно тонко настроить объект, передать параметры, собрать цепочку зависимостей и т.д.). Тогда @Primary вешается на метод:

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

@Configuration
public class NotificationConfig {

    @Bean
    @Primary // По умолчанию выбираем именно этот NotificationSender
    NotificationSender consoleNotificationSender() {
        // Здесь мы регистрируем bean вручную, а не через @Component
        return message -> System.out.println("CONSOLE: " + message);
    }
}

Обратите внимание на важный нюанс: бизнес‑сервис не меняется вообще. Мы не лезем в NotificationDispatchService, не вставляем if/else, не создаём зависимости руками. Мы просто говорим контейнеру: “Когда не уточняют — бери вот это”.

Если вы хотите мысленно зафиксировать идею: @Primary — это “основной путь”, который Spring выбирает по умолчанию, когда тип подходит нескольким beans.

И тут Spring снова ведёт себя как взрослый. Если вы пометите @Primary две реализации одного типа, контейнер не будет выбирать “первичнее первичного”. Он упадёт на старте с понятной ошибкой: “найдено несколько primary beans”. Это именно то, чего мы хотим: лучше пусть приложение не стартует, чем стартует с непредсказуемым выбором.

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

3. @Fallback: запасной вариант

Если @Primary отвечает на вопрос “что брать обычно?”, то @Fallback отвечает на более хитрый вопрос: “Что можно взять, если других нормальных кандидатов нет?”. Это похоже на запаску в багажнике: вы не выбираете её вместо обычного колеса, если всё хорошо, но она спасает, когда иначе вы вообще никуда не едете.

Смысл @Fallback (в человеческой формулировке): если среди кандидатов есть хотя бы один bean без @Fallback, то fallback‑бины считаются “вторым сортом” и проигрывают. Если же оказалось, что кандидаты есть только fallback‑типа, тогда контейнер может выбрать fallback (если он один) и продолжить сборку.

Звучит “вроде как @Primary, только наоборот”, но важно уловить различие: @Primary — это явно назначенный победитель. @Fallback — это кандидат, который согласен быть выбранным только если больше некому.

3.1. No-Op / No-Discount как страховка

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

import org.springframework.stereotype.Component;

@Component
public class LoyalCustomerDiscountPolicy implements DiscountPolicy {
    @Override
    public int apply(int total) {
        // "Настоящая" политика: меняет цену (сделано максимально учебно)
        return total - 10; // очень учебная скидка, не спрашивайте “почему 10”
    }
}

А теперь добавим fallback‑вариант:

import org.springframework.context.annotation.Fallback;
import org.springframework.stereotype.Component;

@Fallback // Этот bean будет выбран только если других кандидатов DiscountPolicy нет
@Component
public class NoDiscountPolicy implements DiscountPolicy {
    @Override
    public int apply(int total) {
        // Запасной вариант: ничего не меняет, но позволяет приложению стартовать
        return total;
    }
}

Если в контейнере есть LoyalCustomerDiscountPolicy, то он будет выбран (потому что он не fallback). NoDiscountPolicy как fallback остаётся “на скамейке запасных”. Но если вы уберёте реальную политику (или временно не зарегистрируете её), тогда fallback станет полезным: контейнер всё равно сможет внедрить DiscountPolicy, и приложение стартанёт.

Здесь новички часто ожидают магии: “Ну раз есть fallback, пусть Spring выберет любой нормальный”. Нет. Если у вас есть два не-fallback кандидата (например, EmailNotificationSender и SmsNotificationSender), а ConsoleNotificationSender помечен @Fallback, контейнер всё равно не сможет выбрать между email и sms. Fallback просто выпадет из гонки, но победителя это не создаст.

То есть @Fallback — это не инструмент выбора между несколькими равноправными реализациями. Это инструмент “дать запасной вариант”, который не мешает, когда есть нормальные кандидаты.

4. Алгоритм выбора: @Primary и @Fallback

Пока не увидишь несколько комбинаций на одной картинке, мозг любит путаться: “Primary — это default, fallback — это тоже default, но как?”. Давайте сделаем мини‑карту решений. Мы не лезем в полный алгоритм резолвинга, нам нужен учебно честный уровень: что происходит в типовых ситуациях, когда сервис просит один bean интерфейсного типа.

Ниже — таблица “что будет, если…”. Считайте, что мы внедряем DiscountPolicy в сервис через конструктор.

Кандидаты в контейнере Что выберет Spring Почему
1 обычный bean Этот bean Нет неоднозначности
2 обычных beans Ошибка Нельзя угадать
1 @Primary + любые другие @Primary Явно назначен победитель
2 @Primary Ошибка Слишком много “главных”
1 обычный + 1 @Fallback Обычный Fallback уступает
Только 1 @Fallback Этот fallback Некому уступать, но кандидат один
2 @Fallback Ошибка Некому уступать, но кандидатов несколько

А чтобы закрепить прям совсем “как процесс”, вот блок‑схема. В ней есть ровно то, что нам нужно на этом дне, без будущих усложнений.

flowchart TD
    %% Упрощённая схема: только кейс "нужен один bean по типу"
    A["Нужен один bean типа T"] --> B["Нашли кандидатов по типу"]
    B -->|0| E["Ошибка: нет bean-а нужного типа"]
    B -->|1| F["Выбрали единственного кандидата"]
    B -->|>1| C["Есть ровно один @Primary?"]
    C -->|да| G["Выбрали @Primary"]
    C -->|нет| D["Есть НЕ-fallback кандидаты?"]
    D -->|да| H["Игнорируем fallback, но если осталось >1 -> ошибка"]
    D -->|нет| I["Все кандидаты fallback: если >1 -> ошибка, если 1 -> выбрать"]

Главная мысль из этой схемы такая: @Primary помогает “выбрать победителя”, а @Fallback помогает “не мешать победителю” и при этом иметь запасной вариант, если победителя нет.

5. Инкремент в ContextFlow

Давайте теперь сделаем самое важное упражнение дня — в голове, а лучше и в коде проекта: мы добавляем несколько реализаций интерфейса, но не превращаем сервисы в фабрики. То есть OrderPlacementService, NotificationDispatchService и OrderPricingService остаются простыми: они про бизнес‑сценарий, а не про выбор конкретных реализаций.

5.1. Default‑канал уведомлений через @Primary

Пусть NotificationDispatchService всё ещё намеренно зависит от одного NotificationSender. На таком контракте проще всего увидеть, как контейнер применяет default-кандидат и при этом не заставляет сервис выбирать реализацию вручную.

import org.springframework.stereotype.Service;

@Service
public class NotificationDispatchService {
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        this.sender = sender;

        // Удобная диагностическая печать: помогает увидеть, что именно внедрилось
        System.out.println("Sender = " + sender.getClass().getSimpleName());
        // Sender = ConsoleNotificationSender
    }
}

Здесь нам важно только одно: сервис просит один sender, а правило выбора живёт снаружи.

Если вы добавите несколько sender‑ов и пометите консольный как @Primary, код сервиса менять не придётся вообще. Это важнейший критерий здоровья архитектуры: выбор реализации происходит “снаружи”, в wiring‑слое.

5.2. DiscountPolicy: основной вариант + fallback‑страховка

Пусть OrderPricingService просит один DiscountPolicy. Мы хотим, чтобы при наличии “умной” политики скидок она побеждала, но чтобы при её отсутствии проект не превращался в кирпич.

import org.springframework.stereotype.Service;

@Service
public class OrderPricingService {
    private final DiscountPolicy discountPolicy;

    public OrderPricingService(DiscountPolicy discountPolicy) {
        // Здесь тоже внедряется ровно один bean DiscountPolicy
        this.discountPolicy = discountPolicy;
    }

    public int price(int total) {
        // Вся логика выбора реализации вынесена в контейнер, сервис просто применяет контракт
        return discountPolicy.apply(total);
    }
}

Дальше мы просто обеспечиваем контейнеру правила выбора: “умная” политика — обычная, “нулевая” — fallback. Всё. Сервис снова не меняется.

5.3. Где это особенно приятно на практике

Когда вы начинаете развивать проект, таких “умолчаний” становится много: где-то нужен один “обычный” канал уведомлений, где-то одна “обычная” политика скидки, где-то один “обычный” writer для аудита. Если каждый сервис будет выбирать реализацию сам, вы получите размазанный по проекту wiring‑хаос. Если же вы задаёте default и fallback через @Primary и @Fallback, то вы получаете два бонуса одновременно: сервисы читаются проще, а контейнер продолжает оставаться центральной точкой сборки приложения.

И да, это тот самый момент, когда Spring перестаёт быть “магией” и становится просто дисциплинированной сборкой объектного графа: вы сами говорите, что главное, а что запасное. Spring лишь следит, чтобы вы не противоречили себе.

6. Типичные ошибки при использовании @Primary и @Fallback

Ошибка №1: ставить @Primary “на всякий случай” и терять читаемость.
Если не проверить, где именно используется default, можно получить ситуацию, когда новый разработчик открывает сервис и не понимает, какая реализация реально внедрится, потому что выбор спрятан где-то в глубине проекта. @Primary хорош тогда, когда выбор действительно массовый и очевидный, а не когда вы “не хотите думать”.

Ошибка №2: пометить два beans как @Primary и удивляться, что стало ещё хуже.
Интуитивно кажется: “ну у меня два главных — пусть Spring выберет один”. Но контейнер так не работает (и слава ему). Два primary — это всё равно неоднозначность, просто в более официальной форме. В результате приложение падает на старте, и это правильно: у типа должен быть один явный default‑победитель.

Ошибка №3: ожидать, что @Fallback разрешит конфликт между двумя обычными реализациями.
@Fallback — это не “слабый primary” и не “выбери любой нормальный”. Это именно запасной вариант. Если у вас два обычных кандидата, контейнер всё равно не знает, какой выбрать. Fallback в такой ситуации лишь гарантированно проиграет, но победителя не создаст.

Ошибка №4: лечить неоднозначность возвратом к new внутри сервиса.
Это самая “душевная” ошибка: вы видите красный stack trace и думаете “да ну его, сейчас просто создам new SmsNotificationSender() и поедем”. Увы, это шаг назад в manual wiring и скрытые зависимости. Выбор реализации должен быть контейнерным, иначе DI превращается в декорацию, а не архитектуру.

Ошибка №5: пытаться использовать @Primary/@Fallback для управления коллекциями beans.
Иногда разработчик внедряет List<NotificationSender> и ожидает, что primary будет “первым” или что fallback “не попадёт в список”. Так думать не стоит: @Primary и @Fallback решают задачу “нужен один bean”. Если вам нужен набор — это отдельный контракт зависимости, и его нужно проектировать отдельно, а не ждать, что primary вдруг начнёт управлять порядком и составом коллекции.

1
Задача
Spring Core, 8 уровень, 1 лекция
Недоступна
Основной отправитель через `@Primary`
Основной отправитель через `@Primary`
1
Задача
Spring Core, 8 уровень, 1 лекция
Недоступна
Резервная политика через `@Fallback`
Резервная политика через `@Fallback`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ