Як створити 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:
- Сканер читає скомпільовані
.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,@Controller) з автоматичним виявленням через@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(). З амперсандом контейнер поверне сам екземпляр фабрики 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): Спричиняє критичне сповільнення підняття контексту застосунку.