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

Як створити Bean в Spring

У Spring Framework існує два базові способи створення бінів у повсякденній розробці:


🟢 Junior Level

1. Основні способи створення бінів

У Spring Framework існує два базові способи створення бінів у повсякденній розробці:

Спосіб 1: Стереотипні анотації (для власного коду)

Клас позначається відповідною анотацією, і Spring автоматично знаходить його під час сканування пакетів (@ComponentScan):

@Service // Автоматично реєструється як бін
public class OrderService {
    public void createOrder() { ... }
}

Спосіб 2: Java-конфігурація через @Bean (для сторонніх бібліотек)

Використовується, коли потрібно зареєструвати бін із класу зовнішньої бібліотеки (код якого неможливо модифікувати анотацією @Component):

@Configuration
public class AppConfig {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

Золоте правило:

  • Власні класи $\to$ @Component, @Service, @Repository, @Controller.
  • Сторонні класи $\to$ @Bean усередині @Configuration.

🟡 Middle Level

1. Спеціалізовані фабрики: інтерфейс FactoryBean<T>

Якщо логіка створення та налаштування об’єкта занадто складна для звичайного методу @Bean (наприклад, вимагає динамічних перевірок, JNDI-пошуку чи створення пулів з’єднань), використовується інтерфейс FactoryBean<T>:

public class SecurityTokenClientFactoryBean implements FactoryBean<SecurityTokenClient> {
    
    @Override
    public SecurityTokenClient getObject() throws Exception {
        // Складна процедура ініціалізації, шифрування або TLS handshake
        return new SecurityTokenClient("https://auth.server", loadKeys());
    }

    @Override
    public Class<?> getObjectType() {
        return SecurityTokenClient.class;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

Особливість отримання біна з контексту:

  • context.getBean("tokenClient") повертає створений цільовий об’єкт SecurityTokenClient.
  • context.getBean("&tokenClient") (із префіксом амперсанда &) повертає безпосередньо сам екземпляр фабрики SecurityTokenClientFactoryBean.

2. Як @ComponentScan знаходить біни: читання через ASM

Spring не завантажує всі класи проєкту в пам’ять JVM (Metaspace) під час сканування!

Клас ClassPathBeanDefinitionScanner використовує бібліотеку низькорівневого аналізу байт-коду ASM:

  1. Сканер читає скомпільовані .class-файли з диска як сирі байтові потоки.
  2. Без завантаження класу через ClassLoader парситься заголовок пулу констант та анотацій.
  3. У разі виявлення @Component формується мета-дескриптор ScannedGenericBeanDefinition.
  4. Це запобігає передчасному вичерпанню пам’яті в Metaspace та суттєво прискорює фазу сканування.

🔴 Senior Level

1. Архітектура @Configuration: Full Mode проти Lite Mode

Поведінка методів із позначкою @Bean принципово залежить від режиму роботи конфігураційного класу:

@Configuration
public class DatabaseConfig {

    @Bean
    public DataSource dataSource() {
        return new HikariDataSource();
    }

    @Bean
    public JdbcTemplate jdbcTemplate() {
        // Прямий виклик іншого @Bean методу!
        return new JdbcTemplate(dataSource());
    }
}

Full Configuration Mode (за замовчуванням: proxyBeanMethods = true):

Spring обгортає клас конфігурації в CGLIB-проксі.

  • Під час виклику dataSource() усередині jdbcTemplate() виклик перехоплюється CGLIB MethodInterceptor.
  • Проксі перевіряє кеш контейнера: якщо екземпляр dataSource уже створено, він повертає наявний синглтон із контексту, не виконуючи тіло методу повторно.

Lite Configuration Mode (proxyBeanMethods = false або оголошення @Bean усередині звичайного @Component):

CGLIB-проксі не генерується.

  • Виклик dataSource() виконується як звичайний прямий Java-виклик методу.
  • Буде створено другий фізичний екземпляр HikariDataSource, що грубо порушує контракт синглтона та спричиняє витік з’єднань пулу!

[!TIP] У Spring Boot 2.2+ для внутрішніх конфігурацій часто використовують @Configuration(proxyBeanMethods = false), оскільки відмова від CGLIB-проксіювання прискорює старт застосунку та економить пам’ять. Проте викликати @Bean-методи один з одного в цьому режимі суворо заборонено!


2. Динамічна реєстрація: ImportSelector та @Enable*

Усі анотації увімкнення модулів і функціональності у Spring (@EnableAsync, @EnableTransactionManagement, @EnableScheduling) побудовані на механізмі ImportSelector:

public class FeatureImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // Динамічний вибір класів для реєстрації на основі умов оточення
        boolean isCloud = Environment.isCloud();
        return isCloud ? 
            new String[] { "com.example.CloudStorageService" } : 
            new String[] { "com.example.LocalStorageService" };
    }
}

// Застосування через мета-анотацію:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(FeatureImportSelector.class)
public @interface EnableStorageFeature { }

3. Програмна реєстрація через Functional API (Spring 5+)

Для середовищ із обмеженим використанням рефлексії (наприклад, компіляція в GraalVM Native Image) Spring 5 надав функціональну реєстрацію без анотацій:

GenericApplicationContext context = new GenericApplicationContext();

// Реєстрація біна через Supplier (без рефлексивного сканування анотацій):
context.registerBean(UserService.class, () -> new UserService(context.getBean(UserRepo.class)));

Цей підхід мінімізує час прогріву JVM і не вимагає збереження метаданих рефлексії в нативному двійковому образі (native image).


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

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

«У Spring біни створюються кількома основними шляхами:

  1. Декларативно: стереотипними анотаціями (@Component, @Service, @Repository, @Controller) з автоматичним виявленням через @ComponentScan.
  2. Конфігураційно: через фабричні методи @Bean усередині класів @Configuration (основний підхід для зовнішніх бібліотек).
  3. Програмно-фабрично: через інтерфейс FactoryBean<T> для складних об’єктів із нетривіальною ініціалізацією.
  4. Динамічно: через ImportSelector / ImportBeanDefinitionRegistrar (механізм усіх анотацій сімейства @Enable*).
  5. Функціонально (Spring 5+): метод registerBean() для скорочення часу старту, AOT та GraalVM Native Image».

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

1. Чому в класі @Configuration виклик @Bean-методу з іншого @Bean-методу не створює новий об’єкт під час кожного звернення?

Відповідь: За замовчуванням класи з анотацією @Configuration працюють у режимі Full Mode (proxyBeanMethods = true). Під час старту контексту Spring генерує CGLIB-підклас конфігураційного класу. Проксі перехоплює виклики @Bean-методів і спочатку перевіряє реєстр синглтонів BeanFactory. Якщо бін уже існує, повертається посилання з кешу, а тіло фабричного методу повторно не виконується.

2. Як отримати сам екземпляр класу FactoryBean, а не створений ним об’єкт?

Відповідь: Під час запиту біна за ім’ям через метод context.getBean("&beanName") необхідно додати префікс амперсанда & до імені біна. Без амперсанда Spring поверне результат виклику методу FactoryBean.getObject(). З амперсандом контейнер поверне сам екземпляр фабрики FactoryBean.

3. У чому різниця між скануванням класів через ASM у @ComponentScan та звичайною рефлексією Class.forName()?

Відповідь: Звичайний виклик Class.forName() завантажує байт-код класу в системний завантажувач (ClassLoader) і розміщує метадані в області пам’яті Metaspace, що призводить до ініціалізації статичних блоків і високого споживання пам’яті. Spring використовує бібліотеку ASM (SimpleMetadataReaderFactory), яка парсить байти файлу .class як сирий бінарний потік, витягуючи інформацію про анотації без завантаження самого класу в JVM.


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

  • ❌ Створення бінів для змінних структур даних (DTO, Entities): Порушує базові принципи архітектури IoC.
  • ❌ Виклик @Bean-методів один з одного при proxyBeanMethods = false: Призведе до створення кількох несинглтонних копій об’єктів і витоків ресурсів.
  • ❌ Твердження, що @Bean можна оголошувати лише всередині @Configuration: Метод @Bean може бути оголошений і в @Component (Lite Mode), але без гарантії повернення синглтона під час міжбінових викликів методів.
  • ❌ Надмірно широке сканування @ComponentScan (наприклад, на весь classpath): Спричиняє критичне сповільнення підняття контексту застосунку.

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