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

Що таке @Configuration клас

У той час як стереотипні анотації (@Service, @Repository, @Component) ставляться безпосередньо над класами вашого проєкту, анотація @Configuration необхідна для:


🟢 Junior Level

@Configuration — це анотація Spring, якою позначається Java-клас, що виступає в ролі фабрики та джерела визначень бінів (BeanDefinition). Усередині такого класу методи позначаються анотацією @Bean.

Навіщо потрібен клас @Configuration

У той час як стереотипні анотації (@Service, @Repository, @Component) ставляться безпосередньо над класами вашого проєкту, анотація @Configuration необхідна для:

  1. Підключення сторонніх бібліотек: ви не можете поставити @Service над класом із чужого JAR-архіву (наприклад, над ObjectMapper із Jackson, AmazonS3 з AWS SDK або HikariDataSource).
  2. Складного програмного налаштування: коли створення об’єкта вимагає виклику кількох сетерів, умов, читання змінних середовища або виклику фабричних методів.

Базовий приклад

package com.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        // Тонке програмне налаштування стороннього класу
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        return mapper;
    }
}

🟡 Middle Level

Розв’язання залежностей: Виклик методу vs Параметри методу

Якщо один бін залежить від іншого, визначеного в тому ж конфігураційному класі, є два способи передачі залежності:

@Configuration
public class SecurityAppConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // Спосіб 1: Прямий виклик методу (Inter-Bean Reference)
    @Bean
    public UserService userService() {
        return new UserService(passwordEncoder()); // CGLIB перехопить виклик!
    }

    // Спосіб 2: Передача через параметри методу (Рекомендований спосіб)
    @Bean
    public AuthService authService(PasswordEncoder passwordEncoder) {
        return new AuthService(passwordEncoder); // Spring сам впровадить синглтон
    }
}

Full Mode vs Lite Mode: Головна архітектурна відмінність

Spring підтримує два режими роботи конфігураційних класів, які визначаються параметром proxyBeanMethods:

Критерій Full Mode (proxyBeanMethods = true) Lite Mode (proxyBeanMethods = false)
Як оголошується @Configuration (за замовчуванням) @Configuration(proxyBeanMethods = false) або @Component з @Bean
Проксіювання Клас загортається в CGLIB-проксі Проксі не створюється (чистий POJO)
Прямий виклик b(a()) Проксі перехоплює виклик a() і повертає наявний синглтон із кешу Виконується звичайний виклик Java-методу → створюється новий дублюючий об’єкт!
Обмеження Java Клас та методи не можуть бути final Класи та методи можуть бути final
Швидкість старту Повільніше (генерація CGLIB-байткоду) Швидше на 30–50%, менші витрати Metaspace
GraalVM Native Потребує підказок рефлексії Повністю Native-friendly з коробки

🔴 Senior Level

Внутрішній механізм Full Mode: ConfigurationClassPostProcessor

На найбільш ранньому етапі старту додатка системний процесор ConfigurationClassPostProcessor (який реалізує BeanDefinitionRegistryPostProcessor) сканує всі класи з @Configuration:

  1. Якщо proxyBeanMethods == true, Spring задіює клас ConfigurationClassEnhancer.
  2. За допомогою CGLIB генерується підклас: SecurityAppConfig$$SpringCGLIB$$0 extends SecurityAppConfig.
  3. У підклас вбудовується перехоплювач BeanMethodInterceptor:
// Спрощена логіка роботи CGLIB BeanMethodInterceptor у Spring Framework:
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
    String beanName = getBeanName(method);

    // Якщо контейнер прямо зараз створює цей бін через фабрику:
    if (isCurrentlyInvokedFactoryMethod(method)) {
        // Викликаємо оригінальний конструктор у суперкласі
        return proxy.invokeSuper(obj, args);
    }

    // Якщо метод викликано з іншого @Bean-методу: перенаправляємо запит до BeanFactory!
    return this.beanFactory.getBean(beanName);
}
Виклик new UserService(passwordEncoder()):
   1. userService() починає виконуватися.
   2. Викликається passwordEncoder().
   3. Виклик перехоплюється CGLIB BeanMethodInterceptor.
   4. Перевіряється singletonObjects cache у BeanFactory.
   5. Повертається ВЖЕ СТВОРЕНИЙ раніше екземпляр BCryptPasswordEncoder.
   6. Новий об'єкт BCryptPasswordEncoder НЕ створюється!

Небезпека Lite Mode: випадкове порушення Singleton-семантики

Якщо в конфігурації з proxyBeanMethods = false розробник за звичкою зв’язує біни прямим викликом:

@Configuration(proxyBeanMethods = false) // Lite Mode
public class BadLiteConfig {

    @Bean
    public DatabasePool pool() {
        return new DatabasePool(); // Екземпляр 1
    }

    @Bean
    public OrderRepo orderRepo() {
        // ПОМИЛКА: Прямий виклик pool() створить ДРУГИЙ незалежний пул з'єднань!
        return new OrderRepo(pool()); 
    }

    @Bean
    public UserRepo userRepo() {
        // ПОМИЛКА: Створить ТРЕТІЙ незалежний пул з'єднань!
        return new UserRepo(pool()); 
    }
}

Результат: замість одного спільного пулу з’єднань додаток відкриє 3 незалежні пули в БД, вичерпавши всі доступні конекти до сервера!

Як писати правильно в Lite Mode:

Усі залежності передаються лише через параметри методів:

@Configuration(proxyBeanMethods = false)
public class CorrectLiteConfig {

    @Bean
    public DatabasePool pool() {
        return new DatabasePool();
    }

    @Bean
    public OrderRepo orderRepo(DatabasePool pool) { // Spring передасть один і той самий синглтон
        return new OrderRepo(pool);
    }

    @Bean
    public UserRepo userRepo(DatabasePool pool) {
        return new UserRepo(pool);
    }
}

@Bean методи всередині звичайного @Component

Якщо оголосити @Bean-метод усередині @Service, @Component або @Controller, Spring обробить його строго в Lite Mode. CGLIB-проксіювання для таких класів не налаштовується. Тому зв’язувати біни прямим викликом методів усередині @Component категорично не можна.


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

30-секундна відповідь

Клас з анотацією @Configuration — це джерело бінів Spring, що містить фабричні методи з анотацією @Bean. Застосовується для інтеграції сторонніх бібліотек та комплексного програмного налаштування. Має два режими:

  1. Full Mode (proxyBeanMethods = true, за замовчуванням): клас огортається в CGLIB-проксі. Прямі виклики @Bean-методів один з одного перехоплюються і повертають зареєстрований синглтон із контейнера. Клас та методи не можуть бути final.
  2. Lite Mode (proxyBeanMethods = false): CGLIB-проксі не створюється, старт швидший, менше витрат пам’яті. Прямі виклики методів породжують нові дублікати об’єктів, тому залежності необхідно передавати через аргументи методів.

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

  1. У чому різниця між Full Mode та Lite Mode в @Configuration? Відповідь: У Full Mode Spring генерує CGLIB-проксі для класу конфігурації, перехоплюючи прямі виклики @Bean-методів для збереження singleton-семантики (beanFactory.getBean()). У Lite Mode CGLIB-проксі відсутній: методи є звичайними Java-методами, а їхній прямий виклик породжує новий екземпляр класу в обхід контексту.

  2. Чому клас з @Configuration у Full Mode не може бути оголошений як final? Відповідь: Full Mode використовує CGLIB для генерації проксі. CGLIB працює через успадкування (створення класу-нащадка $$SpringCGLIB$$). У специфікації Java ключове слово final забороняє успадкування від класу та перевизначення методів. Спроба оголосити @Configuration клас як final призведе до помилки старту контексту.

  3. Що станеться, якщо оголосити метод @Bean усередині класу з анотацією @Component або @Service? Відповідь: Метод буде успішно оброблений, і об’єкт, що повертається, зареєструється як бін. Однак такий клас працюватиме в режимі Lite Mode: прямі виклики сусідніх @Bean-методів не перехоплюватимуться CGLIB, і виклики створять нові неконтрольовані об’єкти.

  4. Навіщо в Spring Boot 2.2+ додали параметр proxyBeanMethods = false? Відповідь: Для оптимізації старту та витрати пам’яті. Вимкнення CGLIB-проксіювання прискорює запуск на десятки відсотків, економить пам’ять Metaspace і є обов’язковою вимогою для ефективної AOT-компіляції у GraalVM Native Image.

Червоні прапорці (чого категорично не можна говорити)

  • ❌ «У Lite Mode анотація @Bean перестає створювати біни» — біни створюються штатно; вимикається лише CGLIB-перехоплення міжбінових викликів.
  • ❌ «У Full Mode виклик @Bean-методу завжди заново виконує тіло методу» — інтерцептор CGLIB повертає закешований синглтон із BeanFactory.
  • ❌ «@Configuration клас можна сміливо робити final» — у Full Mode це викличе помилку JVM під час створення підкласу CGLIB.
  • ❌ «У Lite Mode не можна передавати залежності між @Bean-методами» — можна і потрібно, але через аргументи методів (@Bean public B b(A a)).

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