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

Що таке @Qualifier

Якщо впровадити інтерфейс безпосередньо без уточнення, Spring викине NoUniqueBeanDefinitionException.


🟢 Junior Level

@Qualifier — це анотація Spring Framework, яка усуває неоднозначність під час впровадження залежностей (Dependency Injection), вказуючи точний ідентифікатор або кваліфікатор біна, який потрібно впровадити, якщо в контексті знайдено кілька кандидатів одного типу.

Базовий приклад проблеми та рішення

// Інтерфейс відправлення повідомлень
public interface MessageService { void send(String msg); }

@Component("emailService")
public class EmailService implements MessageService { ... }

@Component("smsService")
public class SmsService implements MessageService { ... }

Якщо впровадити інтерфейс безпосередньо без уточнення, Spring викине NoUniqueBeanDefinitionException.

Використання @Qualifier однозначно вирішує конфлікт:

@Service
public class NotificationManager {

    private final MessageService messageService;

    // Явно вказуємо, який саме бін потрібно впровадити
    public NotificationManager(@Qualifier("emailService") MessageService messageService) {
        this.messageService = messageService;
    }
}

🟡 Middle Level

Розміщення @Qualifier: Декларація vs Точка впровадження

Анотація @Qualifier може застосовуватися у двох місцях життєвого циклу конфігурації:

  1. У точці впровадження (Injection Site): над полем, сетером або параметром конструктора:
    @Autowired
    @Qualifier("fastClient")
    private HttpClient httpClient;
    
  2. У точці оголошення біна (Declaration Site): над класом або @Bean-методом. При цьому ім’я кваліфікатора може не збігатися з ім’ям біна!
    @Configuration
    public class NetworkConfig {
    
        @Bean("primaryHttpClient") // Ім'я біна = primaryHttpClient
        @Qualifier("fastClient")   // Кваліфікатор = fastClient
        public HttpClient httpClient() {
            return new FastHttpClient();
        }
    }
    

    У точці впровадження @Qualifier("fastClient") успішно знайде цей бін, навіть незважаючи на те, що ім’я біна в контексті інше.

@Qualifier vs @Primary vs @Resource

Це класичне питання технічних співбесід:

Анотація Джерело / Специфікація Принцип роботи Пріоритет
@Qualifier Spring (org.springframework...) Пошук byType, потім фільтрація за ім’ям кваліфікатора Найвищий (завжди перевизначає @Primary)
@Primary Spring Позначення біна як вибору за замовчуванням при неоднозначності Нижче за @Qualifier (спрацьовує, лише якщо кваліфікатор не заданий)
@Resource(name="...") Jakarta EE (jakarta.annotation.Resource) Пошук строго за ім’ям біна (byName). Тільки якщо не знайдено — пошук byType Альтернативний стандарт Java EE

🔴 Senior Level

Внутрішній механізм: QualifierAnnotationAutowireCandidateResolver

Перевіркою та зіставленням кваліфікаторів керує системний компонент QualifierAnnotationAutowireCandidateResolver усередині DefaultListableBeanFactory:

  1. Коли резолвер шукає підходящий бін, викликається метод checkQualifier(BeanDefinitionHolder bdHolder, Annotation annotation, ...):
  2. Спочатку перевіряється: чи є у дескриптора біна (BeanDefinition) явно заданий кваліфікатор (AutowireCandidateQualifier), що збігається зі значенням анотації.
  3. Якщо явний кваліфікатор не знайдено, Spring використовує ім’я біна (beanName) та його псевдоніми (aliases) як неявний дефолтний кваліфікатор.
  4. Перевіряються збіги за мета-анотаціями та їхніми атрибутами.

Кастомні типобезпечні кваліфікатори з атрибутами

Використання рядкових значень (@Qualifier("postgresMaster")) схильне до помилок одруків, які неможливо відловити на етапі компіляції.

Створення мета-кваліфікатора з множинними параметрами:

package com.example.annotation;

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

@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Qualifier // Мета-кваліфікатор Spring
public @interface DatabaseTarget {
    Engine engine();
    boolean readOnly() default false;

    enum Engine { POSTGRES, CLICKHOUSE, ORACLE }
}

Використання при оголошенні та впровадженні:

@Configuration
public class DataSourceConfig {

    @Bean
    @DatabaseTarget(engine = DatabaseTarget.Engine.POSTGRES, readOnly = false)
    public DataSource postgresMaster() { return new HikariDataSource(); }

    @Bean
    @DatabaseTarget(engine = DatabaseTarget.Engine.POSTGRES, readOnly = true)
    public DataSource postgresReplica() { return new HikariDataSource(); }
}
@Service
public class ReportingService {

    private final DataSource readOnlyDs;

    // Spring зіставить УСІ атрибути анотації: engine == POSTGRES ТА readOnly == true!
    public ReportingService(
        @DatabaseTarget(engine = DatabaseTarget.Engine.POSTGRES, readOnly = true) DataSource readOnlyDs) {
        this.readOnlyDs = readOnlyDs;
    }
}

Перевага Senior-рівня: Повна типобезпечність під час компіляції, автодоповнення enum-значень в IDE та виключення людських помилок в іменуванні.


🎯 Шпаргалка для інтерв’ю

30-секундна відповідь

@Qualifier — анотація Spring для точкового вибору конкретного біна за наявності кількох кандидатів одного типу.

  • Порядок розв’язання: Spring спочатку знаходить усі біни потрібного типу (byType), а потім відбирає тих, чий кваліфікатор або ім’я збігається зі значенням у @Qualifier.
  • @Qualifier має найвищий пріоритет над @Primary: якщо бін позначений @Primary, але в точці виклику вказано @Qualifier("other"), впроваджено буде саме “other”.
  • Може встановлюватися як у точці ін’єкції, так і в точці оголошення @Bean.
  • Для безпечної архітектури рекомендується створювати кастомні анотації, позначені @Qualifier, що виключає одруки в рядках та забезпечує сувору перевірку типів.

4 каверзних питання з відповідями

  1. У чому принципова відмінність між @Autowired + @Qualifier("db") та @Resource(name = "db")? Відповідь: @Autowired + @Qualifier здійснює пошук за типом (byType), а потім фільтрує кандидатів за кваліфікатором. Анотація @Resource (зі стандарту Jakarta Annotations) здійснює пошук суворо за унікальним ім’ям біна (byName), і тільки якщо бін з таким ім’ям не знайдений, виконує відкат до пошуку за типом.

  2. Що станеться, якщо вказати @Qualifier("foo"), але біна з кваліфікатором “foo” немає, проте є бін з ім’ям “foo”? Відповідь: Бін з ім’ям “foo” буде успішно впроваджений. Spring використовує ім’я біна (beanName) як неявний резервний кваліфікатор за замовчуванням (Fallback Qualifier Matching).

  3. Чи може один і той самий бін мати кілька різних кваліфікаторів? Відповідь: Так. Це реалізується через оголошення кількох кастомних анотацій, кожна з яких позначена мета-анотацією @Qualifier. Клас біна позначається всіма цими анотаціями, і його можна впроваджувати за будь-якою з них.

  4. Як Spring зіставляє кастомні кваліфікатори з кількома полями/атрибутами? Відповідь: Spring зіставляє всі атрибути анотації одночасно. Якщо кастомний кваліфікатор має атрибути format="json" та version=2, ін’єкція відбудеться лише в тому випадку, якщо обидва ці атрибути у біна та в точці впровадження повністю ідентичні.

Червоні прапорці (чого категорично не можна говорити)

  • ❌ «@Primary важливіший, ніж @Qualifier» — груба помилка: явний вибір через @Qualifier завжди має пріоритет над глобальним дефолтом @Primary.
  • ❌ «@Qualifier шукає біни за ім’ям без перевірки типу» — пошук завжди починається з перевірки сумісності типів (byType).
  • ❌ «@Qualifier можна використовувати лише над полями з @Autowired» — анотація встановлюється над параметрами конструктора, сетерами і навіть над @Bean-методами.
  • ❌ «Рядковий @Qualifier кращий за кастомні анотації» — рядки схильні до одруків і ламаються під час рефакторингу.

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