Що робити, якщо є кілька бінів одного типу
Коли в ApplicationContext зареєстровано кілька бінів, що реалізують один і той самий інтерфейс або успадковують один базовий клас, спроба впровадити залежність через @Autowired...
🟢 Junior Level
Коли в ApplicationContext зареєстровано кілька бінів, що реалізують один і той самий інтерфейс або успадковують один базовий клас, спроба впровадити залежність через @Autowired без додаткових вказівок призведе до фатальної помилки:
NoUniqueBeanDefinitionException: No qualifying bean of type 'PaymentService' available: expected single matching bean but found 2: cardPaymentService, paypalPaymentService.
3 основні способи вирішення проблеми
@Primary— призначити бін «за замовчуванням»:@Component @Primary // Цей бін буде обрано, якщо не вказано конкретний кваліфікатор public class CardPaymentService implements PaymentService { } @Component public class PaypalPaymentService implements PaymentService { }@Qualifier— явно вказати ім’я потрібного біна в точці ін’єкції:@Service public class CheckoutService { private final PaymentService paymentService; public CheckoutService(@Qualifier("paypalPaymentService") PaymentService paymentService) { this.paymentService = paymentService; } }- Іменування аргументу (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:
- Spring спробує розв’язати залежність за
@Primary. - Виявивши конфлікт, Spring викине виняток:
NoUniqueBeanDefinitionException: more than one 'primary' bean found among candidates: [beanA, beanB]. - У таких ситуаціях для ранжування застосовують анотацію
jakarta.annotation.Priority(або@Order), або відмовляються від@Primaryна користь явних кваліфікаторів.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
Якщо знайдено кілька бінів одного типу, Spring кидає
NoUniqueBeanDefinitionException. Проблема вирішується такими способами:
@Primary: призначає глобальний бін за замовчуванням для даного типу.@Qualifier("name"): явно вказує ім’я потрібного біна (має найвищий пріоритет над@Primary).- Кастомні кваліфікатори: типобезпечні анотації з
@Qualifier, які захищають від рядкових одруків.- Ін’єкція
Map<String, T>: впроваджує всі реалізації для патерну «Стратегія».@Priority: числове ранжування кандидатів.
4 каверзних питання з відповідями
-
Що станеться, якщо в контексті два біни одного типу позначені
@Primary? Відповідь: Додаток впаде з помилкоюNoUniqueBeanDefinitionException: more than one 'primary' bean found. Наявність кількох бінів з@Primaryдля одного типу є конфліктом конфігурації. -
Що має вищий пріоритет:
@Primaryчи@Qualifier? Відповідь:@Qualifierмає абсолютний пріоритет над@Primary. Анотація@Primaryзадає варіант за замовчуванням, коли жоден кваліфікатор не вказаний. Якщо в точці впровадження явно задано@Qualifier,@Primaryігнорується. -
Як елегантно реалізувати патерн «Стратегія» в Spring Boot? Відповідь: Впровадити
Map<String, StrategyInterface>у конструктор диспетчера. Spring автоматично заповнить мапу всіма бінами даного інтерфейсу, де ключем буде рядковий ідентифікатор біна (beanName), а значенням — сам об’єкт. У рантаймі потрібна стратегія вибирається за $O(1)$ черезmap.get(type). -
Навіщо створювати власні анотації-кваліфікатори (Custom Qualifiers)? Відповідь: Стандартний
@Qualifier("myBean")використовує рядковий літерал. Друкарська помилка в рядку не перевіряється компілятором і призводить до падіння в рантаймі. Користувацька анотація, позначена мета-анотацією@Qualifier, забезпечує сувору перевірку типів під час компіляції, автодоповнення в IDE та підтримку безпечного рефакторингу.
Червоні прапорці (чого категорично не можна говорити)
- ❌ «
@Primaryзавжди перевизначає@Qualifier» — навпаки,@Qualifierмає найвищий пріоритет. - ❌ «Якщо впровадити
Map<String, Service>, то в мапу потрапить тільки один дефолтний бін» — у Map впроваджуються абсолютно ВСІ доступні біни даного типу. - ❌ «Іменування змінної за ім’ям біна — це найкращий і найнадійніший спосіб вибору» — це найбільш крихкий спосіб (fallback), який легко ламається при перейменуванні змінної або вимкненні прапорця компілятора
-parameters. - ❌ «Кілька бінів одного типу взагалі не можна реєструвати в Spring» — це стандартна практика для поліморфізму та патерну «Стратегія».