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

Що робить анотація @Autowired

За замовчуванням @Autowired вимагає обов'язкової наявності біна (required = true). Якщо бін не знайдено, додаток впаде з помилкою NoSuchBeanDefinitionException.


🟢 Junior Level

@Autowired — це анотація Spring Framework, яка вказує IoC-контейнеру автоматично знайти підходящий бін у контексті та впровадити його в залежний клас (механізм Dependency Injection).

Де можна використовувати @Autowired

  1. Над конструктором (Рекомендується):
    @Service
    public class OrderService {
        private final PaymentClient paymentClient;
    
        @Autowired // Якщо конструктор у класі один, анотацію можна не писати (починаючи зі Spring 4.3)
        public OrderService(PaymentClient paymentClient) {
            this.paymentClient = paymentClient;
        }
    }
    
  2. Над сетером (Setter Injection):
    @Autowired
    public void setPaymentClient(PaymentClient paymentClient) {
        this.paymentClient = paymentClient;
    }
    
  3. Над полем (Field Injection, не рекомендується):
    @Autowired
    private PaymentClient paymentClient;
    

Необов’язкові залежності (required = false)

За замовчуванням @Autowired вимагає обов’язкової наявності біна (required = true). Якщо бін не знайдено, додаток впаде з помилкою NoSuchBeanDefinitionException.

Зробити залежність опціональною можна трьома способами:

// Варіант 1: прапорець required
@Autowired(required = false)
private AuditService auditService; // залишиться null, якщо біна немає

// Варіант 2: Java 8 Optional
@Autowired
private Optional<AuditService> auditService;

// Варіант 3: анотація @Nullable
@Autowired
public void setAuditService(@Nullable AuditService auditService) { ... }

🟡 Middle Level

Покроковий алгоритм розв’язання бінів контейнером

Коли Spring бачить точку ін’єкції @Autowired, пошук біна виконується за суворим 5-кроковим алгоритмом:

                          [ Точка впровадження @Autowired ]
                                        │
                                        ▼
                     Крок 1: Пошук усіх кандидатів за ТИПОМ (byType)
                                        │
                      ┌─────────────────┴─────────────────┐
                      ▼                                   ▼
              [ Знайдено рівно 1 ]               [ Знайдено кілька ]
                      │                                   │
                      │                                   ▼
                      │                 Крок 2: Фільтрація через @Qualifier
                      │                                   │
                      │                         ┌─────────┴─────────┐
                      │                         ▼                   ▼
                      │                 [ Знайдено 1 ]      [ Залишилося кілька ]
                      │                         │                   │
                      │                         │                   ▼
                      │                         │       Крок 3: Перевірка @Primary
                      │                         │                   │
                      │                         │         ┌─────────┴─────────┐
                      │                         │         ▼                   ▼
                      │                         │     [ Є @Primary ]      [ Немає @Primary ]
                      │                         │         │                   │
                      │                         │         │                   ▼
                      │                         │         │       Крок 4: Перевірка @Priority
                      │                         │         │                   │
                      │                         │         │         ┌─────────┴─────────┐
                      │                         │         │         ▼                   ▼
                      │                         │         │     [ Є @Priority ]     [ Немає @Priority ]
                      │                         │         │         │                   │
                      │                         │         │         │                   ▼
                      │                         │         │         │     Крок 5: Пошук за ІМ'ЯМ (byName)
                      │                         │         │         │                   │
                      │                         │         │         │         ┌─────────┴─────────┐
                      │                         │         │         │         ▼                   ▼
                      │                         │         │         │   [ Збіглося ім'я ]   [ Не збіглося ]
                      │                         │         │         │         │                   │
                      ▼                         ▼         ▼         ▼         ▼                   ▼
                [ УСПІШНЕ ВПРОВАДЖЕННЯ БІНА ]                                 [ NoUniqueBeanDefinitionException ]
  1. Пошук за типом (byType): Контейнер шукає всі біни, що реалізують потрібний інтерфейс або успадковують клас. Якщо знайдено один — він впроваджується.
  2. Перевірка @Qualifier: Якщо кандидатів кілька, відбираються біни із зазначеним у @Qualifier("beanName") ідентифікатором.
  3. Перевірка @Primary: Якщо кваліфікатора немає, шукається бін, позначений анотацією @Primary.
  4. Перевірка @Priority: Якщо анотації @Primary немає, Spring перевіряє наявність стандартної анотації jakarta.annotation.Priority (обирається кандидат із найменшим числовим значенням).
  5. Пошук за ім’ям поля/параметра (byName fallback): Ім’я змінної або аргументу зіставляється з іменами бінів у контексті.
  6. Помилка: Якщо кандидатів усе ще більше одного, викидається NoUniqueBeanDefinitionException.

Впровадження колекцій та асоціативних масивів

@Autowired дозволяє інжектувати відразу всі реалізації інтерфейсу:

@Service
public class PaymentProcessor {

    // Впроваджуються ВСІ біни, що реалізують PaymentService.
    // Порядок елементів у списку регулюється анотацією @Order!
    @Autowired
    private List<PaymentService> paymentServices;

    // Впроваджується Map: Ключ = ім'я біна в контексті, Значення = екземпляр біна
    @Autowired
    private Map<String, PaymentService> paymentServiceMap;
}

🔴 Senior Level

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

Обробка анотації @Autowired виконується спеціалізованим постпроцесором AutowiredAnnotationBeanPostProcessor (AABPP):

  1. Фаза збору метаданих (MergedBeanDefinitionPostProcessor):
    • Метод postProcessMergedBeanDefinition інспектує клас, знаходить поля та методи з @Autowired, @Value та jakarta.inject.Inject.
    • Метадані кешуються в об’єкті InjectionMetadata (набір дескрипторів AutowiredFieldElement та AutowiredMethodElement), що запобігає повторним витратам на рефлексію при повторному створенні бінів.
  2. Фаза наповнення (InstantiationAwareBeanPostProcessor):
    • На кроці populateBean() викликається метод postProcessProperties().
    • AABPP витягує кешований InjectionMetadata і розв’язує залежності через beanFactory.resolveDependency(...).
    • Для полів рефлексивно викликається field.setAccessible(true) та field.set(bean, dependency).

Чому @Autowired над static полями НЕ працює

Часта помилка розробників-початківців:

@Service
public class UtilityService {
    @Autowired // ПОМИЛКА: dependency буде дорівнювати null!
    private static DatabaseClient client;
}

Чому це відбувається:

AutowiredAnnotationBeanPostProcessor — це процесор екземплярів бінів (Bean Post Processor). Він працює на фазі populateBean() після створення конкретного екземпляра об’єкта. Статичні поля належать самому класу (java.lang.Class), а не екземпляру в купі. Spring навмисно ігнорує статичні поля під час ін’єкції.

Як коректно впровадити в статичний контекст (якщо не можна обійти):

Використовувати нестатичний сетер:

@Service
public class UtilityService {
    private static DatabaseClient client;

    @Autowired
    public void setClient(DatabaseClient client) {
        UtilityService.client = client; // Запис у статичне поле з екземпляра
    }
}

Просунутий інструмент: ObjectProvider<T>

Починаючи зі Spring 4.3 / 5.0, інтерфейс ObjectProvider<T> є кращою альтернативою сирому @Autowired(required = false):

@Service
public class MetricsReporter {

    private final ObjectProvider<PushGatewayClient> clientProvider;

    public MetricsReporter(ObjectProvider<PushGatewayClient> clientProvider) {
        this.clientProvider = clientProvider;
    }

    public void report() {
        // 1. Безпечне отримання (не кидає NoSuchBeanDefinitionException)
        PushGatewayClient client = clientProvider.getIfAvailable();
        if (client != null) {
            client.push();
        }

        // 2. Fallback зі значенням за замовчуванням
        PushGatewayClient safeClient = clientProvider.getIfAvailable(NoOpClient::new);

        // 3. Ітерація по потоку всіх доступних бінів
        clientProvider.orderedStream().forEach(PushGatewayClient::flush);
    }
}

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

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

@Autowired — анотація автоматичного впровадження залежностей (DI) у Spring.

  • Під капотом обробляється компонентом AutowiredAnnotationBeanPostProcessor.
  • Алгоритм пошуку кандидата: byType → @Qualifier → @Primary → @Priority → byName. Якщо кандидатів більше одного — NoUniqueBeanDefinitionException.
  • Рекомендується використовувати Constructor Injection без явної анотації @Autowired (забезпечує імутабельність final, незалежність від Spring-рефлексії та простоту unit-тестів).
  • Підтримує впровадження колекцій List<T> (із сортуванням за @Order) та Map<String, T>.
  • Не працює на static-полях.

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

  1. Який точний порядок розв’язання залежностей під час роботи @Autowired? Відповідь: Спочатку Spring шукає всі біни відповідного типу (byType). Якщо їх кілька, відбираються біни з відповідним @Qualifier. Якщо кваліфікатора немає, обирається бін з анотацією @Primary. Якщо @Primary немає, враховується пріоритет @Priority. Якщо пріоритетів немає, як останній шанс порівнюється ім’я поля/параметра з ім’ям біна (byName). Якщо однозначний кандидат не знайдений, викидається NoUniqueBeanDefinitionException.

  2. Чому @Autowired над статичним полем залишає в ньому null? Відповідь: Spring керує життєвим циклом екземплярів об’єктів у пам’яті. Впровадження залежностей виконується через BeanPostProcessor у фазі populateBean конкретного екземпляра. Статичні поля належать об’єкту Class у ClassLoader, і Spring навмисно пропускає статичні поля під час ін’єкції.

  3. Що станеться, якщо впровадити List<PaymentService>, якщо в контексті є 3 біни цього інтерфейсу? Відповідь: Spring знайде всі 3 біни та впровадить їх у список. Якщо над класами бінів або їхніми @Bean-методами вказана анотація @Order(n) або реалізований інтерфейс Ordered, елементи списку будуть автоматично відсортовані відповідно до заданого порядку (менше число — вищий пріоритет).

  4. У чому різниця між @Autowired(required = false) та ObjectProvider<T>? Відповідь: При @Autowired(required = false) у поле просто записується null, що загрожує появою NullPointerException. ObjectProvider<T> надає типобезпечний API (getIfAvailable(), ifAvailable(Consumer), дефолтні значення через Supplier, ліниве отримання та стримінг бінів через orderedStream()).

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

  • ❌ «Spring шукає біни для @Autowired насамперед за ім’ям поля» — насамперед пошук ЗАВЖДИ йде за типом (byType), а пошук за ім’ям — це лише фінальний fallback.
  • ❌ «@Autowired обов’язковий над кожним конструктором у Spring Boot» — якщо конструктор у класі один, анотація @Autowired не потрібна (починаючи зі Spring 4.3).
  • ❌ «Можна повісити @Autowired на static-поле, і воно нормально заінжектиться» — поле залишиться null.
  • ❌ «Field Injection через @Autowired працює швидше за Constructor Injection» — Field Injection використовує важку рефлексію setAccessible(true) під час кожного старту.

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