🍃 Розділ 5 · Питання #27

Що робити, якщо є кілька бінів одного типу

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


🟢 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» — це стандартна практика для поліморфізму та патерну «Стратегія».

Пов’язані теми