JavaRush /Курси /Spring Core /BeanFactory та

BeanFactory та ApplicationContext

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

1. Два рівні контейнера Spring

Коли починаєте читати про Spring, дуже легко зловити відчуття, що фреймворк спеціально вигадував нові інтерфейси, аби новачкам не було нудно. Але BeanFactory і ApplicationContext — це не два різні Spring, а два рівні однієї й тієї самої моделі. Якщо від самого початку зрозуміти, де фундамент, а де «повсякденний вхід», далі і запуск контексту, і конфігурація, і діагностика сприйматимуться як логічна інженерна система, а не як набір загадкових анотацій.

Тримайте в голові просту думку: Spring вирішує задачу керування об’єктним графом. Але «керування» буває різної глибини. Іноді вам потрібен мінімальний механізм «створи та віддай об’єкт» — це рівень BeanFactory. А іноді потрібен контейнер як середовище виконання застосунку — з конфігурацією, ресурсами, подіями та повідомленнями, — і це вже рівень ApplicationContext.

Візуально (дуже грубо) можна тримати таку картинку в голові:

flowchart TD
    A["Ваш main() / точка входу"] --> B["ApplicationContext (контекст застосунку)"]
    B --> C["BeanFactory (базовий контейнер)"]
    C --> D["Біни (об'єкти застосунку)"]

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

2. BeanFactory: мінімальний контракт “дай мені бін”

BeanFactory зручно сприймати як найпростіший «двигун» контейнера. Ідея проста: у контейнера можна попросити об’єкт bean за ім’ям або за типом, а далі він сам вирішить, як його створити, де взяти залежності та чи потрібно повернути той самий екземпляр, чи новий. Це схоже на дуже розумну фабрику: вона не просто штампує одну деталь, а вміє зібрати цілий виріб, тому що знає, з чого він складається.

Одразу важливе застереження для початківців: BeanFactory — це інтерфейс. Тобто це контракт — набір методів і обіцянок, — а не конкретний клас, який ви зобовʼязані напряму створювати в коді. У реальному житті ви майже завжди працюєте з реалізацією рівня ApplicationContext, але корисно розуміти, що базові операції контейнера — це саме BeanFactory.

Щоб не перетворювати лекцію на довідник, давайте зафіксуємо на рівні змісту кілька методів BeanFactory у вигляді таблиці:

Що ви хочете зробити Як це приблизно виглядає Що це означає людською мовою
Отримати бін getBean(...) «Контейнере, дай мені об’єкт, яким ти керуєш»
Перевірити наявність біна containsBean(name) «Чи існує такий об’єкт у контейнері?»
Зрозуміти, singleton це чи ні isSingleton(name) «Це один екземпляр на контейнер чи кожного разу новий?» (детально про це буде на Дні 4)

Тепер нам достатньо зафіксувати сам принцип. Візьмемо вже піднятий ApplicationContext як факт: один і той самий об’єкт можна читати і як звичайний контекст застосунку, і як нижчий за рівнем BeanFactory.

ApplicationContext context = ...;

BeanFactory factory = context;
String name = factory.getBean("appName", String.class);

System.out.println(name); // ContextFlow

Тут важливий не спосіб, яким був створений context, а межа ролей. BeanFactory дає мінімальний контракт для роботи з біном, але застосунку майже одразу потрібен ширший шар.

3. ApplicationContext: контейнер як середовище виконання застосунку

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

І знову ж, ApplicationContext — це інтерфейс. А конкретна реалізація, яку ми сьогодні використовуємо і детально розглянемо в наступній лекції, — AnnotationConfigApplicationContext. Це контекст, який уміє будуватися на Java-конфігурації.

Якщо трохи підняти кришку та подивитися, що саме додає ApplicationContext, вийде приблизно така карта — без глибини, просто щоб ви не дивувалися назвам пізніше:

Що додає ApplicationContext Навіщо це взагалі потрібно (одним реченням)
Environment Щоб застосунок не був набором жорстко закодованих значень
Resource Щоб читати файли/шаблони/ресурси з classpath без ручної мороки зі шляхами
MessageSource Щоб відокремлювати текст/повідомлення від коду та підтримувати локалі
ApplicationEventPublisher Щоб акуратно розвʼязувати частини застосунку всередині одного процесу
Життєвий цикл застосунку Щоб контейнер умів стартувати/зупинятися та керувати об’єктами як системою

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

Мінімальний приклад: ApplicationContext як “контекст застосунку”

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

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextInfoDemo {
    public static void main(String[] args) {
        // Контекст — це живий об'єкт: у нього є id, displayName і життєвий цикл
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Ідентифікатор конкретного екземпляра контексту (зручно для діагностики)
            System.out.println(context.getId());          // наприклад: org.springframework...

            // Людське ім'я контексту (теж корисно в логах/діагностиці)
            System.out.println(context.getDisplayName()); // наприклад: org.springframework...
        }
    }
}

Вивід буде не надто гарним, але він показує: контекст — це об’єкт, що представляє запущене контейнерне середовище. Він існує, живе, стартує та закривається. І це напряму повʼязано з тим, як працюватиме застосунок.

4. ApplicationContext як точка входу

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

Є ще один важливий момент: ApplicationContext частіше поводиться в дусі fail-fast. Для нормального застосунку це плюс: краще впасти на старті з зрозумілою помилкою, ніж «успішно» запуститися та отримати сюрприз через п’ять хвилин виконання сценарію. Про життєвий цикл, eager і lazy ініціалізацію ми ще поговоримо окремо. Поки просто запам’ятайте правило: для навчання і для більшості застосунків ApplicationContext — більш передбачуваний вибір.

Нарешті, суто по-людськи: ApplicationContext — це та абстракція, навколо якої побудовані звичні приклади та документація. А ще це той шар, який концептуально продовжує composition root: раніше в нас був PlainJavaMain, який вручну створював залежності. Тепер замість нього буде контекст, який створює та зберігає об’єктний граф.

Практично це означає дуже просту річ: входом у застосунок стає ApplicationContext. У bootstrap-шарі ми піднімаємо контекст, отримуємо один стартовий об’єкт і далі передаємо керування звичайному Java-коду. Сам BeanFactory при цьому залишається всередині та робить свою роботу зі створення і видачі бінів.

І тут уже видно наступне інженерне запитання: як підняти такий контекст на Java-конфігурації без Boot і без прихованих кроків. Поки нам важливо лише, чому саме цей рівень стає нормальною дверима в застосунок.

5. Як ApplicationContext використовує BeanFactory

У певний момент початківцю корисно побачити, що ApplicationContext — не «абстрактна надбудова», а цілком конкретна річ, усередині якої є реальний «біновий двигун». Тоді зникає відчуття магії: контекст — це оболонка та інфраструктура, а базова робота зі створення, зберігання та видачі об’єктів — це фабрика. Ми зараз не будемо читати вихідний код Spring — пожалійте себе, — але можемо акуратно подивитися, який «справжній» BeanFactory сидить усередині контексту.

Для цього нам знадобиться інтерфейс вищого рівня: ConfigurableApplicationContext. Він дає доступ до внутрішнього BeanFactory. Візьмемо той самий мінімальний AppConfig і подивимося, який BeanFactory живе всередині контексту.

import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class BeanFactoryInsideDemo {
    public static void main(String[] args) {
        // Беремо більш “низькорівневий” інтерфейс контексту, щоб дістати внутрішній BeanFactory
        try (ConfigurableApplicationContext context =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Це той самий “двигун” контейнера, який реально зберігає визначення бінів
            var beanFactory = context.getBeanFactory();

            // Друкуємо конкретну реалізацію, щоб побачити “справжній клас” усередині
            System.out.println(beanFactory.getClass().getName());
            // наприклад: org.springframework.beans.factory.support.DefaultListableBeanFactory
        }
    }
}

Ось тут відбувається важливий «клац»: контейнер — це не один монолітний об’єкт, а композиція. Контекст керує життєвим циклом застосунку, а BeanFactory — життєвим циклом бінів.

Якщо ви любите візуальні схеми (а їх люблять майже всі, просто не всі в цьому зізнаються), то можна так:

flowchart LR
    AC["AnnotationConfigApplicationContext"] --> BF["BeanFactory (усередині)"]
    BF --> B1["appName (бін String)"]
    BF --> B2["...інші біни пізніше"]

6. getBean() у точці входу та в бізнес-коді

Є дуже підступний момент: щойно ви дізнаєтеся про getBean(), з’являється спокуса вирішувати ним узагалі все. Хочеться прямо з бізнес-коду в будь-який момент діставати потрібний об’єкт із контексту, як із супермаркету: «мені, будь ласка, знижкову політику, сповіщувач і пакетик». Але майже завжди це відкат до тих самих проблем, через які ми взагалі прийшли до DI: прихованих залежностей, жорсткої звʼязаності та поганої тестованості.

На рівні старту застосунку getBean() — абсолютно нормальний інструмент. Зрештою, точка входу має з чогось почати. Але важливо тримати межу: getBean() живе в bootstrap-шарі, а не в бізнес-шарі.

Гарний приклад: getBean() лише в bootstrap-шарі

ApplicationContext context = ...;

// У bootstrap-шарі getBean() доречний: потрібно отримати перший об'єкт сценарію
ScenarioRunner runner = context.getBean(ScenarioRunner.class);
runner.run();

Зміст тут простий: ми один раз отримали стартовий об’єкт і далі живемо звичайним життям Java-застосунку.

Поганий приклад: бізнес-клас сам собі шукає залежності

А ось так робити не треба. Навіть якщо технічно все «працює», код перетворюється на service locator, а залежності знову стають прихованими.

import org.springframework.context.ApplicationContext;

public class OrderPlacementService {

    private final ApplicationContext context;

    public OrderPlacementService(ApplicationContext context) {
        // Погано вже те, що бізнес-сервіс знає про контейнер і тримає його всередині себе
        this.context = context;
    }

    public void placeOrder() {
        // Антипатерн: залежність ховається всередині методу, а не виражена через конструктор
        NotificationSender sender = context.getBean(NotificationSender.class); // так не робимо

        // Бізнес-логіка перемішана з діставанням залежностей із контейнера
        sender.send("Замовлення створено");
    }
}

У чому проблема? Якщо ви відкриєте цей клас і подивитеся на його конструктор, то не побачите реальної залежності від NotificationSender. Вона схована всередині методу. Саме це ми й називали прихованими залежностями ще до Spring. Тільки тепер вони ховаються не за new, а за getBean().

Іронія в тому, що Spring вам був потрібен, щоб прибрати приховані залежності, а не замінити їх іншими прихованими залежностями, просто з красивішою назвою.

Шпаргалка: BeanFactory vs ApplicationContext

Щоб закріпити картину, корисно тримати в голові коротку шпаргалку. Не як список для зубріння, а як розуміння того, що є фундаментом, а що — робочим середовищем.

Критерій BeanFactory ApplicationContext
Головна ідея Мінімальний доступ до бінів Контейнер як середовище виконання застосунку
Рівень Фундамент Щоденний робочий шар
Що вміє точно Створювати та віддавати біни Усе зі BeanFactory + ресурси, події, повідомлення, середовище
Типовий старт застосунку Рідко напряму Майже завжди через ApplicationContext
Роль у курсі Зрозуміти «двигун» Використовувати як вісь курсу та точку входу

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

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

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

Помилка №1: вважати BeanFactory і ApplicationContext синонімами.
На рівні getBean() вони справді схожі, і тому мозок каже: «та яка різниця». Різниця в тому, що BeanFactory — базовий контракт, а ApplicationContext — «контейнер для застосунку» з додатковими можливостями. Якщо ви не бачите цього шару, пізніше з’являться запитання на кшталт «а звідки взялися події, ресурси та повідомлення, чому це теж у контейнері?».

Помилка №2: намагатися почати навчання Spring з низькорівневого API.
Іноді хочеться бути максимально хардкорним і починати з BeanFactory, DefaultListableBeanFactory та ручної реєстрації. Це швидко перетворюється на читання документації замість навчання, тому що ви додаєте багато механіки ще до того, як побачили користь. Набагато продуктивніше запускатися через ApplicationContext, але розуміти, що він спирається на BeanFactory.

Помилка №3: використовувати getBean() як повсякденний спосіб отримувати залежності в бізнес-коді.
Це виглядає як зручність — «мені ж просто потрібен один об’єкт», — але архітектурно це відкат до прихованих залежностей і жорсткої звʼязаності. У нашому курсі getBean() доречний у main() як інструмент старту і іноді як інструмент діагностики, але не як спосіб проєктувати сервіси.

Помилка №4: не закривати контекст.
Поки ми не обговорюємо життєвий цикл, може здаватися, що закриття контексту — формальність. Але звичка закривати контекст через try-with-resources — це акуратний професійний рефлекс. Він стане в пригоді, коли з’являться біни, яким справді потрібне коректне завершення роботи, і це буде в модулі про життєвий цикл.

Помилка №5: думати, що ApplicationContext «сам усе знайде» без реєстрації конфігурації.
Контейнер не телепат. Йому потрібно явно пояснити, які класи вважати конфігурацією та які біни реєструвати. Сьогодні ми тримаємо все максимально явно: Java-конфігурація, @Configuration, @Bean, і зрозумілий запуск AnnotationConfigApplicationContext. Це дисципліна, яка рятує від відчуття «магії».

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