🍃 Раздел 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 лучше кастомных аннотаций» — строки подвержены опечаткам и ломаются при рефакторинге.

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