JavaRush /Курси /Spring Core /BeanDefinition — пас...

BeanDefinition — паспорт біна

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

1. Роль BeanDefinition у контейнері

Коли вперше бачите Spring, легко подумати: «Ну окей, це велика фабрика, яка робить new замість мене». Але цінність контейнера не в цьому. Він керує системою обʼєктів, а не просто створює один-два екземпляри. А щоб керувати системою, контейнеру потрібна карта місцевості: хто є хто, як створювати, як звʼязувати, коли створювати і які правила застосовувати до створення.

Ми вже написали AppConfig, підняли AnnotationConfigApplicationContext і викликали getBean(). Між конфігурацією та готовим обʼєктом залишався прихований крок: контейнер не перескакує одразу від @Bean‑методу до екземпляра. Спочатку він реєструє BeanDefinition, а вже потім створює реальні обʼєкти.

Уявіть, що вам потрібно приготувати вечерю на 15 людей. Можна «просто почати різати овочі» — тобто одразу створювати обʼєкти, — але значно практичніше спочатку подивитися рецепти, скласти список продуктів, зрозуміти порядок дій і хто від кого залежить: спершу розігріти духовку, потім поставити страву, потім сервірувати. У Spring роль цих «рецептів і плану» виконує саме шар метаданих, і головна одиниця цього шару називається BeanDefinition.

BeanDefinition як «паспорт» біна

Якщо зовсім по-людськи, BeanDefinition — це опис того, що контейнер має вміти створити, як він має це створити і які правила до цього обʼєкта застосовні. Це не ваш OrderPlacementService і не рядок "ContextFlow", а «картка» — метадані, — за якою Spring пізніше створить реальний обʼєкт. Саме тому в документації Spring BeanDefinition називають основним поданням метаданих: контейнер спочатку працює з метаданими, а вже потім — з реальними обʼєктами.

Важливо відчути цю думку інтуїтивно: у застосунку існують два різні світи. В одному живуть реальні обʼєкти: String, сервіси, сховища. В іншому — описи: «бін із таким-то імʼям створюється таким-то методом із такої-то конфігурації, singleton, ліниво чи ні, і так далі». Spring значну частину своєї «магії» робить саме в другому світі, поки перший ще навіть не почався.

Трохи формальніше: BeanDefinition — це інтерфейс із пакета org.springframework.beans.factory.config. Найчастіше всередині будуть конкретні реалізації на кшталт RootBeanDefinition, але на нашому рівні достатньо памʼятати: інтерфейс описує метадані, і ці метадані лежать у BeanFactory.

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

Що описує BeanDefinition Як це проявляється в житті
Імʼя біна За ним контейнер зберігає і знаходить визначення. Сьогодні це "appName", завтра — сервіси проєкту.
Спосіб створення «Створити через конструктор» або «створити через фабричний метод». Для @Bean це зазвичай фабричний метод.
factoryBeanName Імʼя біна, на якому буде викликано фабричний метод, наприклад бін конфігурації appConfig.
factoryMethodName Імʼя методу, який треба викликати, наприклад appName.
Scope За замовчуванням singleton. Пізніше говоритимемо про prototype, але зараз просто знаємо, що такий прапорець існує.
Lazy-init та інші прапорці Можна відкласти створення або, навпаки, створити обʼєкт на старті. Ми це докладно розберемо пізніше, але «слот» для цього є вже зараз.
Аргументи конструктора / значення властивостей Метадані для інʼєкції залежностей. Навіть якщо ми поки не обговорюємо способи інʼєкції, контейнеру все одно потрібно зберігати «як зібрати обʼєкт».

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

2. Джерела BeanDefinition для @Configuration і @Bean

На попередніх лекціях ми працювали у звичайній Java і збирали граф вручну. Тепер уже запускаємо контейнер, і тут головне не пропустити один крок: коли ви пишете @Configuration і @Bean, Spring не «вшиває магію в Java». Він робить значно прозаїчнішу річ: читає вашу конфігурацію і перетворює її на набір BeanDefinition. Спочатку будує метадані, а вже потім за ними створює обʼєкти.

Давайте візьмемо мінімальний приклад, який продовжує наш сьогоднішній застосунок і водночас залишається простим. Нехай у нас є AppConfig, який описує два біни: імʼя застосунку і невеликий банер, який використовує це імʼя.

package com.example.contextflow.config;

import com.example.contextflow.support.StartupBanner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Конфігурація Spring: саме її контейнер читає і перетворює на BeanDefinition.
 */
@Configuration
public class AppConfig {

    @Bean
    public String appName() {
        // Цей метод не просто "утиліта": він стає фабричним методом біна.
        return "ContextFlow";
    }

    @Bean
    public StartupBanner startupBanner(String appName) {
        // Залежність appName буде розвʼязана контейнером за типом/імʼям.
        return new StartupBanner(appName);
    }
}

А StartupBanner — звичайний Java-клас, без Spring:

package com.example.contextflow.support;

public class StartupBanner {
    private final String appName;

    public StartupBanner(String appName) {
        // Це "звичайний" конструктор: Spring тут не потрібен, це просто залежність.
        this.appName = appName;
    }

    public void print() {
        // Читабельніше, ніж конкатенація: одразу видно майбутній рядок виведення.
        String message = """
                === %s запущено ===
                """.formatted(appName).trim();

        System.out.println(message); // === ContextFlow запущено ===
    }
}

Тепер ключове: після старту контексту в контейнера будуть визначення для appName і startupBanner. І окремо, уже у «світі обʼєктів», — екземпляр String і екземпляр StartupBanner.

Ще один момент, який корисно знати заздалегідь, щоб не дивуватися: у контексті зʼявляться не лише ваші визначення. Spring додає і свої інфраструктурні визначення, без яких не запрацювали б анотації конфігурації. Сьогодні ми не пірнаємо в ці внутрішні механізми, але сам факт корисний для моделі в голові: «контейнер більший за мій код». Пізніше ми до цього повернемося значно глибше.

3. Дві фази: definitions і instances

Дуже типова проблема новачка в Spring виглядає так: «Я нічого не викликав, а в мене вже виняток на старті, і stack trace на три екрани». І ось тут BeanDefinition різко перетворюється з теорії на рятівне коло.

На базовому рівні зручно тримати в голові таку картину: контейнер робить дві великі роботи. Спочатку він збирає карту — definitions, — а потім починає будувати місто — instances. Так, у реальності refresh() складніший і багатоступеневий, але нам сьогодні потрібна робоча модель, а не енциклопедія.

Накреслимо схему — спрощену, але чесну за змістом:

flowchart TD
    A["Ви пишете AppConfig (@Configuration + @Bean)"] --> B["Контекст запускається"]
    B --> C["Читає конфігурацію та реєструє BeanDefinition"]
    C --> D["Перевіряє, чи можна зібрати граф (знаходить залежності)"]
    D --> E["Створює обʼєкти (біни) за визначеннями"]
    E --> F["Ви отримуєте стартовий бін через getBean() і запускаєте сценарій"]

Чому це важливо на практиці? Тому що багато помилок — це не «помилка в бізнес-коді», а помилка в картині збирання. Наприклад, якщо ви описали бін startupBanner(String appName), але забули описати бін appName(), то проблема виникне ще до того, як ви викличете banner.print(). Контейнер спробує зібрати граф і скаже: «Я не можу зрозуміти, звідки взяти String appName».

І це хороша поведінка. Це і є підхід fail-fast: краще впасти під час старту, ніж «якось» запуститися і зірватися посеред сценарію, коли користувач уже повірив, що все працює.

4. Як подивитися BeanDefinition у коді

Зараз буде найкорисніший фрагмент лекції: ми зробимо BeanDefinition відчутним. Бо поки це слово живе лише в тексті, мозок новачка закономірно думає: «А можна мені, будь ласка, помацати це палицею?». Можна.

Нижче — маленький main(), який запускає контекст, бере BeanDefinition і друкує кілька полів. Зверніть увагу: ми ліземо в BeanFactory не тому, що так треба писати бізнес-код, а тому, що сьогодні ми вивчаємо внутрішню картину.

package com.example.contextflow;

import com.example.contextflow.config.AppConfig;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Application {
    public static void main(String[] args) {
        // Важливо: створення контексту = запуск процесу побудови визначень та екземплярів.
        try (AnnotationConfigApplicationContext context =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Дістаємо не бін, а "паспорт" біна за ім'ям.
            BeanDefinition def = context.getBeanFactory().getBeanDefinition("appName");

            // Дивимося, хто і яким методом створюватиме цей бін.
            System.out.println(def.getFactoryBeanName());    // appConfig
            System.out.println(def.getFactoryMethodName());  // appName
        }
    }
}

Що ми зараз побачили? Не String "ContextFlow", а «паспорт»: «бін appName створюється фабричним методом appName на біні appConfig». Це дуже сильна думка: Spring памʼятає не лише що створити, а й звідки це взялося.

Давайте поруч подивимося на інший бін — startupBanner:

// Так само можна подивитися "паспорт" іншого біна.
BeanDefinition bannerDef =
        context.getBeanFactory().getBeanDefinition("startupBanner");

System.out.println(bannerDef.getFactoryBeanName());    // appConfig
System.out.println(bannerDef.getFactoryMethodName());  // startupBanner

А тепер — важливий звʼязок «опис vs обʼєкт». Спершу отримаємо обʼєкт і викличемо його:

import com.example.contextflow.support.StartupBanner;

// Це вже "світ обʼєктів": контейнер віддає готовий екземпляр.
StartupBanner banner = context.getBean(StartupBanner.class);
banner.print(); // === ContextFlow запущено ===

Зазвичай новачок після цього думає: «Ну ось же, я отримав обʼєкт. Навіщо мені взагалі визначення?». А потім приходить життя, де потрібно діагностувати помилки, керувати конфігурацією і розуміти, звідки взявся той чи інший бін. І ось там BeanDefinition стає як ліхтарик у темному підвалі: без нього можна, але страшно й боляче.

Щоб сильніше відчути різницю, корисно додати маленький println() всередину @Bean‑методу і подивитися, коли він спрацьовує:

@Bean
public String appName() {
    // Цей вивід допоможе побачити момент фактичного створення singleton-біна.
    System.out.println("створюю appName");  // створюю appName
    return "ContextFlow";
}

Якщо ви запускаєте контекст звичайним способом, то побачите, що цей println() спрацює під час старту, тому що singleton-біни за замовчуванням створюються одразу. Але BeanDefinition існує ще до того, як ви почнете користуватися обʼєктом у бізнес-сценарії. Контейнер, по суті, уже знає: «ось рецепт», і вже вирішив: «я зараз приготую».

5. Практична цінність шару метаданих

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

Наприклад, контейнеру корисно заздалегідь зібрати весь граф залежностей і впасти, якщо чогось не вистачає. Саме це й дає той самий підхід fail-fast, який пояснює, чому помилки виникають «у момент старту».

Ще контейнеру корисно мати стандартну модель, у якій визначення можуть приходити з різних джерел. Сьогодні в нас це @Configuration і @Bean. У наступні дні зʼявляться й інші способи реєстрації, але контейнеру все одно потрібен один спільний формат: «ось опис майбутнього біна». BeanDefinition тут — універсальна мова, на яку Spring «перекладає» різні джерела.

І нарешті, шар метаданих робить можливим керування життєвим циклом обʼєктів не за схемою «один раз створив і забув», а як повноцінним середовищем виконання. Навіть якщо ми поки не обговорюємо життєвий цикл і scopes, уже зараз корисно розуміти: контейнер — це не бібліотека «зроби new за мене». Контейнер — це «я керую обʼєктами як середовищем виконання». І без шару описів він просто не зміг би робити це передбачувано.

Тут важливо втримати межу курсу: сьогодні ми не розбираємо, які саме механізми розширюють і модифікують BeanDefinition під час старту. Нам достатньо зрозуміти, що Spring спочатку будує контейнерну картину, а потім матеріалізує її в обʼєкти.

Невелика практика для закріплення

Зараз хороший момент зробити мікроексперимент у вашому проєкті, не змінюючи предметну область ContextFlow — її перенесення в Spring-контекст буде в наступній лекції. Сенс експерименту простий: додати ще один @Bean і подивитися, що в нього в BeanDefinition. Це закріплює думку, що @Bean — не «створення обʼєкта», а «реєстрація того, що обʼєкт може і має існувати».

Додайте в AppConfig такий бін, який даватиме поточну «версію конфігурації». Найпримітивніший варіант — просто число:

@Bean
public Integer configVersion() {
    return 1;
}

А потім у main() подивіться на його визначення:

BeanDefinition v = context.getBeanFactory().getBeanDefinition("configVersion");

System.out.println(v.getFactoryBeanName());    // appConfig
System.out.println(v.getFactoryMethodName());  // configVersion

І окремо отримайте сам обʼєкт:

Integer version = context.getBean("configVersion", Integer.class);
System.out.println(version); // 1

Якщо ви зараз зловили себе на думці «ага, визначення — це про те, як створити», значить лекція спрацювала. У наступній лекції ми зробимо те саме, але вже не на іграшкових String/Integer, а на справжніх компонентах ContextFlow: сховищах і сервісах.

7. Типові помилки під час розуміння BeanDefinition

Помилка № 1: вважати, що BeanDefinition — це «ще один обʼєкт застосунку».
Часто новачки бачать слово definition і думають, що це якийсь «обʼєкт-обгортка навколо біна». Але BeanDefinition — це метадані, «картка рецепта», а не суп у тарілці. Якщо плутати опис і екземпляр, далі дуже легко заплутатися, чому контейнер може сваритися ще до того, як ви викликали бізнес-методи.

Помилка № 2: намагатися будувати застосунок через getBeanDefinition() так, ніби це нормальний прикладний API.
Так, сьогодні ми чесно залізли в BeanFactory і дивилися визначення — але це навчальна діагностика. Якщо перенести такий стиль у бізнес-код, вийде service locator із тонною прихованих залежностей: класи почнуть «питати в контейнера, що там є», замість того щоб отримувати залежності явно. Це як намагатися керувати компанією, постійно читаючи лише реєстр співробітників, але ніколи не призначаючи конкретних людей на конкретні задачі.

Помилка № 3: не помічати, що старт контексту — це окрема стадія життя застосунку.
Іноді здається, що new AnnotationConfigApplicationContext(AppConfig.class) — це просто «створили обʼєкт контексту». Насправді ви запускаєте цілий процес: читання конфігурації, реєстрацію визначень, побудову картини, створення singleton-бінів. Якщо не відокремлювати стадію «контейнер стартує» від стадії «мої сценарії запускаються», будь-який stack trace сприйматиметься як хаос.

Помилка № 4: думати, що @Bean — це «просто метод, який я колись викличу».
Дуже поширена думка: «Ну це ж звичайний Java-метод, отже контейнер викличе його тоді, коли я попрошу». Але контейнер у більшості випадків створює singleton-біни на старті й викликає @Bean‑методи в межах свого процесу. Правильна модель тут така: @Bean‑метод — це частина опису складання контейнера, а не «утилітний метод на кожен день».

Помилка № 5: лякатися того, що в контексті більше бінів, ніж ви написали.
Якщо ви випадково роздрукуєте context.getBeanDefinitionNames() і побачите десятки імен, легко подумати: «Мене зламали, всередині контейнера хтось живе». Насправді це нормальна інфраструктура Spring, яка забезпечує роботу анотацій і конфігурації. Ми ще дійдемо до пояснення того, чому це так влаштовано, але вже зараз корисно прийняти: контейнер — це середовище виконання, а не просто «ваші класи плюс трохи клею».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ