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, яка забезпечує роботу анотацій і конфігурації. Ми ще дійдемо до пояснення того, чому це так влаштовано, але вже зараз корисно прийняти: контейнер — це середовище виконання, а не просто «ваші класи плюс трохи клею».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ