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

Що таке Bean в Spring

У традиційному Java-коді розробник керує об'єктами вручну за допомогою оператора new:


🟢 Junior Level

1. Що таке Spring Bean простими словами

Spring Bean (Бін) — це будь-який об’єкт Java, створенням, конфігурацією, впровадженням залежностей та повним життєвим циклом якого управляє IoC-контейнер Spring (представлений інтерфейсом ApplicationContext).

У традиційному Java-коді розробник керує об’єктами вручну за допомогою оператора new:

// Звичайний об'єкт (POJO): розробник сам створює екземпляр і стежить за ним
User user = new User("Іван");

У Spring управління об’єктом делегується фреймворку:

// Spring Bean: об'єкт створюється, налаштовується та управляється Spring-контейнером
@Service
public class UserService {
    // Spring сам створить екземпляр, впровадить залежності та зареєструє в контейнері
}

2. В чому різниця між звичайним об’єктом (POJO) та Spring Bean?

Будь-який Spring Bean технічно є звичайним Java-об’єктом (POJO), але наділяється контейнером додатковими можливостями:

Характеристика Звичайний Java-об’єкт (POJO) Spring Bean
Створення Вручну розробником через new Автоматично Spring-контейнером
Впровадження залежностей Ручна передача аргументів Автоматичне через DI (@Autowired / конструктор)
Життєвий цикл Залежить від збирача сміття (GC) Керується контейнером (@PostConstruct, @PreDestroy)
Проксіювання (AOP) Звичайний клас Може бути обгорнутий у CGLIB/JDK-проксі (@Transactional, @Async)
Область видимості (Scope) Визначається областю видимості посилання Керується Scope (singleton, prototype, request, тощо)

🟡 Middle Level

1. Рецепт створення біна: BeanDefinition

До того як об’єкт фізично з’явиться в оперативній пам’яті (Heap), Spring сканує проєкт і формує для кожного майбутнього біна спеціальний мета-дескриптор — BeanDefinition:

@Component / @Bean / XML
           │
           ▼
[ BeanDefinition (Мета-рецепт) ]
  • Ім'я цільового Java-класу
  • Scope (singleton, prototype...)
  • Аргументи конструктора та властивості
  • Імена init- та destroy-методів
  • Прапорці lazyInit, primary, dependsOn
           │
           ▼
[ Instantiation & Configuration ] ──► Фізичний екземпляр Bean у пам'яті

BeanDefinition — це «креслення» або «рецепт», за яким фабрика BeanFactory згодом інстанціює реальний об’єкт. Розробник може перехоплювати та модифікувати ці рецепти до створення екземплярів бінів за допомогою інтерфейсу BeanFactoryPostProcessor.


2. Способи оголошення бінів

Існує три основні підходи до реєстрації бінів у Spring:

  1. Стереотипні анотації (Component Scanning):
    • @Component — базовий будівельний блок.
    • @Service — сервісний шар бізнес-логіки.
    • @Repository — шар доступу до даних (додатково транслює SQL-винятки в ієрархію DataAccessException).
    • @Controller / @RestController — шар обробки HTTP-запитів.
  2. Java-based конфігурація (@Configuration + @Bean): Використовується для налаштування класів сторонніх бібліотек, які неможливо розмітити анотаціями безпосередньо:
    @Configuration
    public class AppConfig {
        @Bean
        public ObjectMapper objectMapper() {
            return new ObjectMapper().findAndRegisterModules();
        }
    }
    
  3. Функціональна реєстрація (Spring 5+): Програмна реєстрація через лямбда-вирази в GenericApplicationContext.registerBean(...).

🔴 Senior Level

1. Архітектура зберігання бінів у ядрі Spring

Центральною реалізацією фабрики бінів у Spring є клас DefaultListableBeanFactory.

Усередині нього задіяна трирівнева система кешів синглтонів (DefaultSingletonBeanRegistry):

  1. singletonObjects (Map<String, Object>): кеш першого рівня, що зберігає повністю готові, ініціалізовані та обгорнуті в проксі синглтон-біни.
  2. earlySingletonObjects (Map<String, Object>): кеш другого рівня, що містить ранні посилання на ще не до кінця ініціалізовані біни для розв’язання циклічних залежностей.
  3. singletonFactories (Map<String, ObjectFactory<?>>): кеш третього рівня, що зберігає фабрики для створення ранніх посилань або AOP-проксі у разі виникнення циклічних залежностей.

Усі сховища всередині DefaultListableBeanFactory побудовані на базі потокобезпечних структур даних (ConcurrentHashMap).


2. Бін як AOP-проксі: пастка ключового слова final

У більшості корпоративних проєктів на Spring Boot бін, впроваджений через @Autowired або конструктор, — це не прямий екземпляр вашого класу, а динамічний проксі-об’єкт, який перехоплює виклики:

Клієнт ──► [ UserService$$SpringCGLIB$$0 (Proxy) ] ──► [ UserService (Real Object) ]
                 │
                 ├── Перехоплення виклику (MethodInterceptor)
                 ├── Відкриття транзакції (TransactionInterceptor)
                 ├── Виклик реального методу
                 └── Фіксація (commit) або відкат (rollback) транзакції

Критична пастка для Senior-розробника: За замовчуванням у Spring Boot 2.x+ використовується створення проксі через CGLIB (генерація класу-нащадка в рантаймі через бібліотеку Byte Buddy/ASM).

  • Якщо клас оголошений як final, CGLIB не може створити підклас (це призведе до аварійного завершення під час підняття контексту).
  • Якщо окремий бізнес-метод оголошений як final:
    @Service
    public class PaymentService {
        @Transactional
        public final void processPayment() { // final-метод!
            // Логіка переказу коштів
        }
    }
    

    Код скомпілюється і застосунок успішно запуститься. Проте CGLIB не здатний перевизначити final-метод! Під час виклику processPayment() проксі-перехоплювач буде повністю проігнорований, транзакція не відкриється, а бізнес-логіка виконається у небезпечному режимі без автоматичного відкату при помилках.


3. Які об’єкти НЕ повинні бути Spring-бінами?

Груба архітектурна помилка — намагатися реєструвати всі класи проєкту як Spring-біни:

  1. Сутності баз даних (JPA Entities): User, Order мають власний життєвий цикл, прив’язані до сесії persistence context / Hibernate і створюються через ORM або new.
  2. Об’єкти передачі даних (DTO) та Records: DTO — це прості структури даних без складної поведінки.
  3. Об’єкти-значення (Value Objects): незмінні типи на зразок Money, Address.
  4. Короткоживучі контекстні об’єкти: важкі тимчасові контексти обробки запитів, які мають швидко видалятися збирачем сміття (GC).

Бін у Spring — це насамперед компонент бізнес-архітектури або інфраструктури (Stateless Service, Repository, Web Client, Controller, Configuration).


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

30-секундна відповідь (Elevator Pitch)

«Spring Bean — це звичайний Java-об’єкт (POJO), життєвим циклом якого повністю управляє IoC-контейнер Spring: він створює його за мета-описом BeanDefinition, впроваджує залежності через DI, обгортає в AOP-проксі за наявності анотацій на зразок @Transactional чи @Async та викликає методи життєвого циклу (@PostConstruct, @PreDestroy). На відміну від звичайного POJO, біни найчастіше є бездержавними (stateless) і виступають архітектурними компонентами (сервіси, репозиторії, контролери). Об’єкти даних (Entity, DTO, Records) бінами бути не повинні».


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

1. Чим відрізняється об’єкт, створений через new UserService(), від біна UserService, отриманого зі Spring-контексту?

Відповідь: Об’єкт, створений через new, є простим ізольованим екземпляром у Heap: його поля залежностей дорівнюють null (якщо не передані в конструктор явно), функціонал анотацій Spring (@Transactional, @Async, @Cacheable) на ньому не активується, а callback-методи життєвого циклу (@PostConstruct) не викликаються. Бін із контексту повністю ініціалізований контейнером, містить усі впроваджені залежності та, як правило, обгорнутий у динамічний AOP-проксі для декларативного керування транзакціями та безпекою.

2. Що станеться, якщо позначити клас з анотацією @Service ключовим словом final у Spring Boot?

Відповідь: У сучасному Spring Boot за замовчуванням увімкнено CGLIB-проксіювання (proxy-target-class=true). Якщо клас позначено як final, CGLIB не зможе згенерувати підклас у рантаймі. Якщо на біні немає жодного аспекту (@Transactional, @Async, @Secured), контекст може запуститися. Проте якщо бін потребує проксіювання, Spring викине BeanCreationException із причиною Cannot subclass final class.

3. Що таке BeanDefinition і в який момент він створюється?

Відповідь: BeanDefinition — це інтерфейс ядра Spring, який описує метадані майбутнього біна (ім’я класу, область видимості scope, фабричні методи, залежності, прапорець lazy-init). Метадані формуються парсерами анотацій (@Component, @Bean) або XML на етапі сканування пакетів (ComponentScan), задовго до того, як самі об’єкти класів будуть фізично інстанційовані в пам’яті. Вони реєструються в BeanDefinitionRegistry і можуть бути модифіковані через BeanFactoryPostProcessor.


Типові помилки та Red Flags

  • ❌ Створення бінів для DTO або JPA Entities: Біни призначені для сервісів та інфраструктурних компонентів, а не для передачі або представлення структур даних.
  • ❌ Використання ключового слова final на методах з @Transactional: CGLIB-проксі не зможе перевизначити метод, і анотація мовчки перестане працювати без виклику перехоплювача.
  • ❌ Твердження, що Bean — це особливий тип класу в Java: Будь-який POJO-клас може стати біном; різниця полягає виключно в тому, хто керує створенням та життєвим циклом об’єкта.
  • ❌ Прямий виклик new для біна в коді бізнес-логіки: Якщо створити @Service через new, у ньому не працюватимуть впроваджені залежності та функціонал проксі-обгорток.

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