Как создать 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:
- Сканер читает скомпилированные
.class-файлы с диска как байтовые потоки. - Без загрузки класса через
ClassLoaderпарсится заголовок констант и аннотаций. - При обнаружении
@Componentформируется мета-дескрипторScannedGenericBeanDefinition. - Это предотвращает утечки памяти в 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()вызов перехватывается CGLIBMethodInterceptor. - Прокси проверяет кэш контекста: если экземпляр
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 бины создаются несколькими путями:
- Декларативно: стереотипными аннотациями (
@Component,@Service,@Repository) с автоматическим обнаружением через@ComponentScan.- Конфигурационно: через фабричные методы
@Beanвнутри классов@Configuration(основной путь для внешних библиотек).- Программно-фабрично: через интерфейс
FactoryBean<T>для сложных объектов с нетривиальной инициализацией.- Динамически: через
ImportSelector/ImportBeanDefinitionRegistrar(механизм всех@Enable*аннотаций).- Функционально (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): Приводит к критическому замедлению старта приложения.