🍃 Раздел 5 · Вопрос #27

Что делать, если есть несколько бинов одного типа

Когда в ApplicationContext зарегистрировано несколько бинов, реализующих один и тот же интерфейс или наследующих один базовый класс, попытка внедрить зависимость через @Autowire...


🟢 Junior Level

Когда в ApplicationContext зарегистрировано несколько бинов, реализующих один и тот же интерфейс или наследующих один базовый класс, попытка внедрить зависимость через @Autowired без дополнительных указаний приведет к фатальной ошибке: NoUniqueBeanDefinitionException: No qualifying bean of type 'PaymentService' available: expected single matching bean but found 2: cardPaymentService, paypalPaymentService.

3 основных способа решения проблемы

  1. @Primary — назначить бин «по умолчанию»:
    @Component
    @Primary // Этот бин будет выбран, если не указан конкретный квалификатор
    public class CardPaymentService implements PaymentService { }
    
    @Component
    public class PaypalPaymentService implements PaymentService { }
    
  2. @Qualifier — явно указать имя требуемого бина в точке инъекции:
    @Service
    public class CheckoutService {
        private final PaymentService paymentService;
    
        public CheckoutService(@Qualifier("paypalPaymentService") PaymentService paymentService) {
            this.paymentService = paymentService;
        }
    }
    
  3. Именование аргумента (byName fallback):
    @Service
    public class CheckoutService {
        // Имя параметра совпадает с именем бина в контексте
        public CheckoutService(PaymentService paypalPaymentService) { ... }
    }
    

🟡 Middle Level

Иерархия приоритетов разрешения неоднозначности

При поиске подходящего бина среди нескольких кандидатов Spring руководствуется строгой иерархией:

1. @Qualifier (Явное указание имени/квалификатора — НАИВЫСШИЙ приоритет)
             ↓
2. @Primary (Если квалификатор не задан, выбирается дефолтный бин)
             ↓
3. @Priority (jakarta.annotation.Priority — выбор бина с наименьшим числом)
             ↓
4. Поиск по имени поля/параметра (Fallback: совпадение имени переменной с ID бина)
             ↓
5. NoUniqueBeanDefinitionException (Если однозначный кандидат не определен)

Критическое правило: @Qualifier всегда сильнее @Primary. Если один бин помечен @Primary, но в точке внедрения указан @Qualifier("otherBean"), Spring без колебаний внедрит именно otherBean.

Паттерн «Стратегия» через инъекцию Map

Вместо выбора одного бина Spring позволяет внедрить все реализации интерфейса в виде Map<String, T> или List<T>. Это эталонный способ реализации паттерна Стратегия (Strategy Pattern):

package com.example.service;

import org.springframework.stereotype.Service;
import java.util.Map;

@Service
public class PaymentDispatcher {

    // Spring автоматически внедрит мапу, где:
    // Ключ (String) = имя бина ("cardPaymentService", "paypalPaymentService")
    // Значение = экземпляр сервиса
    private final Map<String, PaymentService> paymentServices;

    public PaymentDispatcher(Map<String, PaymentService> paymentServices) {
        this.paymentServices = paymentServices;
    }

    public void processPayment(String paymentType, Long amount) {
        PaymentService service = paymentServices.get(paymentType);
        if (service == null) {
            throw new IllegalArgumentException("Неподдерживаемый способ оплаты: " + paymentType);
        }
        service.pay(amount); // O(1) диспетчеризация без громоздких switch / if-else!
    }
}

🔴 Senior Level

Типобезопасные кастомные квалификаторы (Custom Qualifiers)

Использование строковых литералов в @Qualifier("paypalService") опасно: опечатка в имени бина не обнаруживается компилятором и приводит к ошибке только во время старта приложения.

Решение: Аннотации-мета-квалификаторы

Создаем собственную аннотацию, помеченную мета-аннотацией @Qualifier:

package com.example.annotation;

import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier // Мета-квалификатор Spring!
public @interface FastPay {
}

Применение на стороне бина и точки внедрения:

// 1. Помечаем бин
@Component
@FastPay
public class QuickCardPaymentService implements PaymentService { ... }

// 2. Внедряем с типобезопасной проверкой компилятора
@Service
public class OrderService {
    private final PaymentService paymentService;

    public OrderService(@FastPay PaymentService paymentService) {
        this.paymentService = paymentService; // 100% защита от опечаток!
    }
}

Продвинутые квалификаторы с атрибутами

Кастомный квалификатор может содержать Enum-атрибуты для многомерного выбора:

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface DataSourceSelector {
    DatabaseType value();
}

public enum DatabaseType { POSTGRES, ORACLE, CLICKHOUSE }
@Bean
@DataSourceSelector(DatabaseType.CLICKHOUSE)
public DataSource analyticsDataSource() { ... }

@Service
public class AnalyticsService {
    public AnalyticsService(@DataSourceSelector(DatabaseType.CLICKHOUSE) DataSource ds) { ... }
}

Что будет, если два бина помечены @Primary?

Если в контексте присутствуют два бина одного типа, и оба помечены аннотацией @Primary:

  1. Spring попытается разрешить зависимость по @Primary.
  2. Обнаружив конфликт, Spring выбросит исключение: NoUniqueBeanDefinitionException: more than one 'primary' bean found among candidates: [beanA, beanB].
  3. В таких ситуациях для ранжирования применяют аннотацию jakarta.annotation.Priority (или @Order), либо отказываются от @Primary в пользу явных квалификаторов.

🎯 Шпаргалка для интервью

30-секундный ответ

Если найдено несколько бинов одного типа, Spring бросает NoUniqueBeanDefinitionException. Проблема решается следующими способами:

  1. @Primary: назначает глобальный бин по умолчанию для данного типа.
  2. @Qualifier("name"): явно указывает имя требуемого бина (имеет наивысший приоритет над @Primary).
  3. Кастомные квалификаторы: типобезопасные аннотации с @Qualifier, защищающие от строковых опечаток.
  4. Инъекция Map<String, T>: внедряет все реализации для паттерна «Стратегия».
  5. @Priority: числовое ранжирование кандидатов.

4 каверзных вопроса с ответами

  1. Что произойдет, если в контексте два бина одного типа помечены @Primary? Ответ: Приложение упадет с ошибкой NoUniqueBeanDefinitionException: more than one 'primary' bean found. Наличие нескольких бинов с @Primary для одного типа является конфликтом конфигурации.

  2. Что имеет более высокий приоритет: @Primary или @Qualifier? Ответ: @Qualifier имеет абсолютный приоритет над @Primary. Аннотация @Primary задает вариант по умолчанию, когда никакой квалификатор не указан. Если в точке внедрения явно задан @Qualifier, @Primary игнорируется.

  3. Как элегантно реализовать паттерн «Стратегия» в Spring Boot? Ответ: Внедрить Map<String, StrategyInterface> в конструктор диспетчера. Spring автоматически заполнит мапу всеми бинами данного интерфейса, где ключом будет строковый идентификатор бина (beanName), а значением — сам объект. В рантайме нужная стратегия выбирается за $O(1)$ через map.get(type).

  4. Зачем создавать собственные аннотации-квалификаторы (Custom Qualifiers)? Ответ: Стандартный @Qualifier("myBean") использует строковый литерал. Опечатка в строке не проверяется компилятором и приводит к падению в рантайме. Пользовательская аннотация, помеченная мета-аннотацией @Qualifier, обеспечивает строгую проверку типов во время компиляции, автодополнение в IDE и поддержку безопасного рефакторинга.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «@Primary всегда переопределяет @Qualifier» — наоборот, @Qualifier обладает наивысшим приоритетом.
  • ❌ «Если внедрить Map<String, Service>, то в мапу попадет только один дефолтный бин» — в Map внедряются абсолютно ВСЕ доступные бины данного типа.
  • ❌ «Именование переменной по имени бина — это лучший и самый надежный способ выбора» — это самый хрупкий способ (fallback), который легко ломается при переименовании переменной или отключении флага компилятора -parameters.
  • ❌ «Несколько бинов одного типа вообще нельзя регистрировать в Spring» — это стандартная практика для полиморфизма и паттерна «Стратегия».

Связанные темы