JavaRush /Курси /Spring Core /Помилки пошуку бінів у Spring

Помилки пошуку бінів у Spring

Spring Core
Рівень 4 , Лекція 4
Відкрита

1. Stack trace як мова контейнера

Stack trace — це мова, якою контейнер пояснює, чому не зміг зібрати застосунок. Ми вже бачили, що він падає ще під час refresh(), якщо на етапі створення не може зібрати граф залежностей. Тепер важливо навчитися читати це падіння, а не просто дивитися на червоний stack trace на пів екрана.

Коли ви вперше бачите довгий stack trace на пів екрана, мозок чесно намагається зробити вигляд, що нічого не сталося, і пропонує перезапустити застосунок «на удачу». Але Spring у такі моменти не шкідить і не «ламає вам життя» — він робить рівно те, що повинен: каже, що граф об’єктів не збирається. Якби контейнер промовчав і «якось стартував», ви отримали б зламаний застосунок, який падає не на старті, а посеред сценарію, коли користувач уже думає, що все працює.

Важливо перебудувати ставлення: помилка старту — це не катастрофа, а діагностичний звіт. Контейнер показує, який бін він намагався створити, яку залежність хотів підставити та чому не зміг. Наше завдання — перекласти цей звіт із мови винятків на людську: «не зареєстровано бін X», «знайшлося два кандидати замість одного», «бін A не зібрався, тому що бін B не зібрався…». Після кількох таких розборів Spring перестає бути «чорною скринькою» і стає дуже балакучим колегою.

Три базові ситуації

Майже всі перші падіння контейнера в новачків укладаються в три сюжети. На старті це дуже полегшує життя: бачите виняток — і одразу приблизно розумієте, у який бік копати. Давайте зафіксуємо це в компактній таблиці, щоб у голові з’явилася «карта місцевості», а не відчуття безкінечного лісу помилок.

Ситуація Як виглядає в Spring Що це означає людською мовою
Потрібного біну немає NoSuchBeanDefinitionException «Ви просите AuditWriter, а я не знаю, звідки його взяти — ви його не зареєстрували».
Підходящих бінів кілька NoUniqueBeanDefinitionException «Ви просите NotificationSender, а їх у мене два або три. Я не телепат і не хочу вгадувати».
Бін не зібрався, тому що зламалася залежність UnsatisfiedDependencyException «Я намагався створити ScenarioRunner, але він залежить від OrderPlacementService, а той залежить від AuditWriter, якого немає або якого занадто багато».

Важливий нюанс: UnsatisfiedDependencyException часто лякає найбільше, тому що звучить як «щось не задоволено, усе погано». Але в більшості випадків це просто обгортка, яка каже: «я не зміг зібрати ось цей бін, тому що всередині його залежностей є справжня проблема». Справжня причина зазвичай ховається в одному з Caused by.

Де саме спливає проблема

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

під час пошуку — контекст уже стартував, а ви самі викликали context.getBean(...) і попросили те, чого контейнер не може віддати;
під час старту контексту — контейнер натрапив на ту саму проблему під час refresh(), коли створював інший бін.

У другому випадку зверху ви частіше побачите UnsatisfiedDependencyException, тому що ламається вже не ваш явний пошук, а збирання залежного біна. Але корінь один і той самий: контейнер не зміг видати потрібний об’єкт — або його немає, або підходящих кандидатів занадто багато.

2. Контейнер не знайшов потрібний бін

Спочатку подивімося на найпряміший варіант: контекст уже стартував, а ви самі просите бін, якого там немає. Ця помилка — як повідомлення від комірника: ви прийшли за коробкою з написом AuditWriter, а він відкриває склад і каже: «Такої коробки в мене немає. Можливо, ви забули її привезти». У Spring це означає, що контейнер не знайшов бін за вашим запитом — за типом або за іменем. Гарна новина в тому, що це одна з найчесніших помилок: зазвичай вона прямо каже, що саме не знайдено.

Почнімо з найпростішого варіанта: ви намагаєтеся отримати бін за типом, а його не зареєстрували.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Просимо бін за типом: якщо в контексті немає bean definition для AuditWriter, тут упадемо
    context.getBean(AuditWriter.class); // якщо біна немає — буде NoSuchBeanDefinitionException
}

Тут важливо розуміти, що контейнер шукає не «клас десь у проєкті», а bean definition усередині контексту. Якщо в AppConfig не було реєстрації AuditWriter, то контейнеру просто нічого повертати.

Якщо цей самий відсутній бін потрібен не вам напряму, а іншому біну під час refresh(), форма помилки буде довшою: зверху часто опиниться UnsatisfiedDependencyException, а в корені все одно залишиться той самий NoSuchBeanDefinitionException. Цей ланцюжок ми розберемо трохи нижче на прикладі падіння під час старту.

Друга часта різновидність — пошук за іменем. Особливо боляче, коли ви впевнені, що «ім’я точно таке саме», а там описка або інший стиль іменування.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // Шукаємо за іменем: одна описка — і контейнер вважає, що біна не існує
    context.getBean("auditWritter"); // описка: Writter замість Writer
}

У таких випадках повідомлення винятку зазвичай прямо каже щось на кшталт “No bean named auditWritter available”. І це якраз той рідкісний момент, коли читати текст винятку — не порожня формальність, а найкоротший шлях до істини.

Ще один варіант, який спливає напрочуд часто: ви зареєстрували бін, але просите не той тип. Наприклад, у контексті є ConsoleAuditWriter, а ви просите FileAuditWriter. З погляду контейнера це нормальна ситуація: ви запитуєте конкретний тип, про який він не знає.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
    // У контексті може бути ConsoleAuditWriter, але якщо просимо FileAuditWriter, це інший тип
    context.getBean(FileAuditWriter.class); // а у вас лише ConsoleAuditWriter
}

Тут допомагає пам’ятати просте правило: пошук за типом — це пошук за сумісним типом. Якщо в контексті є бін, який можна присвоїти змінній потрібного типу, наприклад реалізація інтерфейсу, він підійде. Якщо ні — контейнер вважає, що такого біна не існує.

3. Контейнер знайшов кілька кандидатів

Ця помилка зазвичай з’являється рівно в той момент, коли ви робите щось цілком нормальне: додаєте другу реалізацію інтерфейсу. Для ContextFlow це взагалі природний шлях: один AuditWriter пише в консоль, інший — у файл; один NotificationSender надсилає email, інший — SMS. Контейнер не проти, але йому потрібно, щоб вибір був однозначним. Якщо вибір не однозначний, він падає, тому що вгадувати Spring не буде.

Подивімося на мінімальний приклад. Ми оголосили два біни одного інтерфейсу і пробуємо отримати один за типом.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AmbiguousAppConfig {

    @Bean
    public AuditWriter consoleAuditWriter() {
        // Перший кандидат на роль AuditWriter
        return new ConsoleAuditWriter();
    }

    @Bean
    public AuditWriter fileAuditWriter() {
        // Другий кандидат на роль AuditWriter
        return new FileAuditWriter();
    }
}

Тепер ось такий код:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AmbiguousAppConfig.class)) {
    // Просимо "один AuditWriter", але контейнер бачить два кандидати — тому падає
    context.getBean(AuditWriter.class); // NoUniqueBeanDefinitionException
}

Логіка контейнера тут проста: за типом AuditWriter підходять два біни. А метод getBean(AuditWriter.class) — це запит «дай мені один». Контейнер не може вирішити, який із двох «правильніший», і тому пише помилку виду “expected single matching bean but found 2: consoleAuditWriter,fileAuditWriter”.

Дуже часта помилка новачка в цей момент — сподіватися, що alias вирішить проблему. Але alias — це просто друга назва того самого біна. Він не перетворює два біни на один і не допомагає вибрати «головний». Якщо в контейнері лежать два різні екземпляри, alias не робить ситуацію менш неоднозначною.

Тут нам поки достатньо побачити сам симптом неоднозначності. Але важливо не зробити з цього хибне правило: кілька реалізацій одного інтерфейсу — нормальна частина дизайну. Тоді контейнеру просто потрібен явний спосіб вибору кандидата — через @Primary, @Qualifier, @Fallback або ін’єкцію колекцій бінів.

Іноді для діагностики корисно явно запросити бін за іменем, щоб переконатися, що обидва справді існують і що проблема саме в неоднозначності. Це виглядає так:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

try (var context = new AnnotationConfigApplicationContext(AmbiguousAppConfig.class)) {
    // Явно вибираємо бін за іменем: так можна підтвердити, що обидва біни справді зареєстровані
    AuditWriter writer = context.getBean("consoleAuditWriter", AuditWriter.class);
    System.out.println(writer.getClass().getSimpleName()); // ConsoleAuditWriter
}

Зверніть увагу: це «лікує» ситуацію лише в тому місці, де ви робите пошук. Якщо ж неоднозначність знаходиться всередині зв’язування, наприклад якийсь бін залежить від AuditWriter, то контекст упаде ще на старті, і до вашого getBean("consoleAuditWriter", AuditWriter.class) справа не дійде. У цьому й сенс fail-fast: контейнер не хоче запускати застосунок із незрозумілим зв’язуванням.

4. «Падає один бін, але винен інший»

Тепер візьмімо варіант падіння на старті: ви самі нічого не просите з контексту, але refresh() не може завершитися, тому що один бін тягне за собою зламану залежність.

На перший погляд цей виняток здається найшкідливішим. Ви запускаєте застосунок, він падає, і там написано щось на кшталт “Error creating bean with name 'scenarioRunner'” — і хочеться йти лагодити ScenarioRunner. Але це часто хибний слід. UnsatisfiedDependencyException означає: контейнер намагався створити бін і не зміг задовольнити одну з його залежностей. Тобто проблема зазвичай не в самому біні, а в тому, що він просить у контейнера щось, чого контейнер не може дати.

Уявімо ланцюжок у ContextFlow: ScenarioRunner залежить від OrderPlacementService, той залежить від AuditWriter. А AuditWriter ми забули зареєструвати. Тоді «падатиме» ScenarioRunner, хоча він сам може бути написаний ідеально.

Нижче граф спеціально урізано до одного проблемного ребра: ScenarioRunnerOrderPlacementServiceAuditWriter. Решту залежностей сервісу опускаємо, тому що зараз важливий сам ланцюжок падіння.

Ось невеликий приклад конфігурації, де залежність відсутня:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BrokenAppConfig {

    @Bean
    public ScenarioRunner scenarioRunner(OrderPlacementService service) {
        // ScenarioRunner не можна зібрати, якщо не зібрано OrderPlacementService
        return new ScenarioRunner(service);
    }

    @Bean
    public OrderPlacementService orderPlacementService(AuditWriter auditWriter) {
        // Решту залежностей сервісу тут не показуємо: нам важливо побачити одне відсутнє ребро графа
        return new OrderPlacementService(auditWriter);
    }

    // Бін AuditWriter відсутній: тут корінь проблеми
}

Що буде насправді? Контекст спробує створити ScenarioRunner, побачить, що йому потрібен OrderPlacementService, почне створювати OrderPlacementService, побачить, що тому потрібен AuditWriter, і ось тут усе зламається. Тому зверху ви побачите UnsatisfiedDependencyException про ScenarioRunner або про OrderPlacementService, а внизу, ближче до кореня, буде NoSuchBeanDefinitionException про AuditWriter.

Схематично такий ланцюжок часто виглядає так:

// Приклад: зверху — обгортки (хто "падає"), знизу — корінь проблеми (чого бракує)
UnsatisfiedDependencyException: Error creating bean 'scenarioRunner'
Caused by: UnsatisfiedDependencyException: Error creating bean 'orderPlacementService'
Caused by: NoSuchBeanDefinitionException: No qualifying bean of type 'AuditWriter' available

Тут найважливіша звичка дня: не зупинятися на першому, найверхньому, винятку. Верхній виняток показує, який бін контейнер намагався зібрати в момент падіння. Але причина майже завжди лежить глибше.

5. Алгоритм і діагностика помилок старту

Міні-алгоритм читання помилки старту

Якщо щоразу розбиратися в stack trace «на око», мозок дуже швидко втомиться. Набагато корисніше мати маленький механічний алгоритм: як відкрити stack trace і за хвилину зрозуміти, це «немає біна» чи «бін не обрано», і де саме. Такий алгоритм не замінює розуміння, але робить діагностику спокійнішою: ви не боретеся з хаосом, а йдете крок за кроком.

Уявіть, що винятки — це матрьошка. Ззовні велика матрьошка — UnsatisfiedDependencyException, всередині — менша, і десь у самому центрі сидить справжній винуватець: NoSuchBeanDefinitionException або NoUniqueBeanDefinitionException. Шукаємо не зовнішню матрьошку, а ту, що в центрі.

Нижче — проста схема, як я зазвичай пояснюю це новачкам.

flowchart TD
    A[Контекст не стартує] --> B[Дивимося на верхній виняток: який бін падає]
    B --> C[Пролистуємо вниз до першого Caused by]
    C --> D[Повторюємо: шукаємо наступний Caused by]
    D --> E{У самому низу який тип?}
    E -->|NoSuchBeanDefinitionException| F[Біна не знайдено: перевіряємо реєстрацію, імʼя та тип]
    E -->|NoUniqueBeanDefinitionException| G[Знайдено кілька: робимо залежність однозначною]
    E -->|Інше| H[Читаємо повідомлення: параметр конструктора/методу, імʼя біна, потрібний тип]

На практиці це виглядає так: ви знаходите в stack trace блоки Caused by, рухаєтеся до найнижчого, де вже не «Error creating bean X», а конкретне “No qualifying bean of type ...” або “expected single matching bean but found ...”. Після цього повертаєтесь трохи вище і дивитеся, хто саме просив цю залежність: який бін і який параметр. Там часто прямо написано “Unsatisfied dependency expressed through parameter 0” — це і є підказка, яка саме залежність не підставилася.

Діагностичні прийоми: «що контейнер взагалі бачить усередині себе»

Коли ви вже зрозуміли тип проблеми — немає біна або кандидатів занадто багато, — іноді хочеться швидко переконатися, що контейнер справді зареєстрував те, що ви думаєте. Особливо корисно це в момент, коли ви впевнені, що AuditWriter «точно є», а Spring каже, що немає. У такій ситуації краще не сперечатися з контейнером — він упертий, — а запитати його: «Гаразд, а які AuditWriter ти взагалі бачиш?»

В ApplicationContext є методи, які чудово підходять для такої діагностики. Головне — пам’ятати, що це не повсякденний стиль бізнес-коду, а тимчасовий ліхтарик для налагодження зв’язування.

import org.springframework.context.ApplicationContext;

import java.util.Arrays;

public class ContextDiagnostics {

    public static void printAuditWriters(ApplicationContext context) {
        // Запитуємо в контейнера список імен бінів, які підходять за типом AuditWriter
        String[] names = context.getBeanNamesForType(AuditWriter.class);

        // Якщо масив порожній — немає жодного кандидата; якщо елементів кілька — буде неоднозначність
        System.out.println(Arrays.toString(names)); // наприклад: [consoleAuditWriter]
    }
}

Якщо names порожній, значить контейнер справді не зареєстрував жодного AuditWriter. Тоді йдете в конфігурацію і шукаєте, де він мав би з’явитися. Якщо names містить два імені, а ви отримували NoUniqueBeanDefinitionException, значить контейнер не бреше: кандидатів справді кілька.

Схожий прийом іноді корисний для діагностики «не того імені»:

import org.springframework.context.ApplicationContext;

public class NameDiagnostics {

    public static void checkBean(ApplicationContext context, String beanName) {
        // Швидка перевірка: чи є в контейнері бін із такою назвою
        boolean exists = context.containsBean(beanName);

        // Корисно, коли підозрюєте описку або інший стиль іменування
        System.out.println(beanName + " exists = " + exists); // auditWriter exists = false
    }
}

Такі перевірки допомагають новачку швидко приземлитися в реальність: не гадати, а подивитися, що саме бачить контейнер. Але повторю важливу думку: якщо подібні методи розповзаються по бізнес-сервісах, виходить код у стилі service locator, який складніше тестувати та підтримувати. Тому тримаємо діагностику в точці старту або в окремому налагоджувальному місці, а після виправлення зв’язування — прибираємо.

6. Типові помилки під час діагностики

Помилки діагностики часто виглядають не як «поганий Java-код», а як погана звичка читати повідомлення контейнера. Через це людина може годинами лагодити не той клас, сперечатися з винятком і додавати випадкові getBean() у надії «ну хоч якось запуститься». Нижче — найпопулярніші граблі, на які наступають майже всі, і це нормально. Головне — вчасно впізнавати їх за слідами.

Помилка № 1: дивитися лише на верхній рядок stack trace.
Найверхніший рядок майже завжди каже, який бін контейнер намагався створити останнім. Це схоже на ситуацію «упала стеля, значить винна стеля». Насправді причина часто сидить унизу, в Caused by, де написано «не знайдено AuditWriter» або «знайдено два NotificationSender». Звичка шукати найнижчий Caused by економить величезну кількість часу.

Помилка № 2: лагодити бін, який «упав», а не залежність, через яку він не зібрався.
UnsatisfiedDependencyException часто змушує лагодити ScenarioRunner, тому що саме він фігурує в повідомленні. Але «зламався» він зазвичай тому, що контейнер не зміг зібрати його залежності. Тому в голові потрібно тримати об’єктний граф: ScenarioRunnerOrderPlacementServiceAuditWriter. І лагодити саме там, де ланцюжок рветься.

Помилка № 3: плутати «біна немає» і «бінів занадто багато».
NoSuchBeanDefinitionException і NoUniqueBeanDefinitionException звучать схоже, але зміст у них протилежний. У першому випадку контейнер каже «дай мені хоч один», а в нього нуль. У другому — «дай мені один», а в нього два. Виправлення теж різні, і спроба «додати ще один бін» при NoUnique... робить лише гірше.

Помилка № 4: думати, що наявність класу в проєкті означає наявність біна в контейнері.
Новачки часто мислять так: «У мене ж є ConsoleAuditWriter.java, чому Spring його не бачить?» Тому що контейнер бачить не файли в проєкті, а зареєстровані bean definitions. Поки ви не сказали контейнеру «ось це — бін», він так і залишиться просто класом на диску.

Помилка № 5: намагатися «швидко виправити» все, розсипавши context.getBean(...) по бізнес-коду.
Іноді після кількох помилок з’являється спокуса: «ну гаразд, я сам у сервісі буду діставати потрібний бін за іменем». Так застосунок справді може стартувати, але ви непомітно перетворюєте DI на service locator, а залежності знову стають прихованими — тільки тепер вони ховаються в getBean() замість new. Для навчального проєкту і для нормального зростання архітектури це поганий шлях.

1
Задача
Spring Core, 4 рівень, 4 лекція
Недоступна
Помилка lookup для відсутнього bean-а
Помилка lookup для відсутнього bean-а
1
Задача
Spring Core, 4 рівень, 4 лекція
Недоступна
Головна причина збою запуску
Головна причина збою запуску
1
Опитування
Біни Spring, рівень 4, лекція 4
Недоступний
Біни Spring
Біни й контекст
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ