🍃 Раздел 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 {
        // Сложная процедура инициализации, шифрования или 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) с автоматическим обнаружением через @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(). С амперсандом контейнер вернет саму реализацию фабрики.

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): Приводит к критическому замедлению старта приложения.

Связанные темы