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

Коли Spring створює proxy

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


🟢 Junior Level

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

Основні тригери створення проксі

  1. AOP-анотації (наскрізна функціональність):
    • @Transactional — керування транзакціями бази даних.
    • @Async — асинхронне виконання в окремому пулі потоків.
    • @Cacheable, @CachePut, @CacheEvict — кешування результатів методів.
    • @PreAuthorize, @Secured — перевірка прав доступу Spring Security.
    • @Validated на рівні класу сервісу — валідація параметрів методів (Hibernate Validator).
  2. Конфігураційні класи (@Configuration):
    • Для перехоплення прямих викликів @Bean-методів один з одного зі збереженням семантики Singleton.
  3. Області видимості (Scoped Proxies):
    • При ін’єкції короткоживучих бінів (@RequestScope, @SessionScope) у довгоживучі синглтони.
  4. Інтерфейси Spring Data JPA:
    • Для автоматичної реалізації репозиторіїв (JpaRepository, CrudRepository), у яких немає Java-класу з вихідним кодом.
  5. Ліниве завантаження залежностей (@Lazy):
    • Для відкладання реальної ініціалізації біна до моменту першого звернення до його методів.
package com.example.service;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    // Тригер 1: Транзакційний інтерцептор
    @Transactional
    public void updateUser() { }

    // Тригер 2: Кешуючий інтерцептор
    @Cacheable("users")
    public User findById(Long id) { return null; }

    // Тригер 3: Асинхронний перехоплювач потоків
    @Async
    public void sendEmailNotification() { }
}

🟡 Middle Level

5 сценаріїв створення проксі в архітектурі Spring

                             ┌───────────────────────────────────────────────┐
                             │       Коли Spring генерує Проксі?             │
                             └───────────────────────┬───────────────────────┘
                                                     │
         ┌───────────────────┬───────────────────────┼───────────────────────┬───────────────────┐
         ▼                   ▼                       ▼                       ▼                   ▼
 ┌───────────────┐   ┌───────────────┐       ┌───────────────┐       ┌───────────────┐   ┌───────────────┐
 │   Spring AOP  │   │ @Configuration│       │ Scoped Proxy  │       │  Spring Data  │   │     @Lazy     │
 │  Interceptors │   │    Classes    │       │ (Web Scopes)  │       │  Repositories │   │  Dependencies │
 ├───────────────┤   ├───────────────┤       ├───────────────┤       ├───────────────┤   ├───────────────┤
 │@Transactional │   │ proxyBean-    │       │ @RequestScope │       │ JpaRepository │   │ Відкладене    │
 │@Async, @Cache │   │ Methods=true  │       │ @SessionScope │       │ CrudRepository│   │ створення біна│
 └───────────────┘   └───────────────┘       └───────────────┘       └───────────────┘   └───────────────┘

1. Spring AOP та кастомні аспекти

Проксі створюється, якщо сигнатура методу або клас біна підпадає під Pointcut будь-якого аспекту:

  • Декларативні анотації ядра: @Transactional, @Async, @Cacheable, @Retryable.
  • Користувацькі аспекти, оголошені через @Aspect бібліотеки AspectJ (наприклад, @Around("@annotation(LogExecutionTime)")).

2. Класи конфігурації @Configuration(proxyBeanMethods = true)

За замовчуванням кожен @Configuration-клас у Spring обгортається в CGLIB-проксі:

@Configuration(proxyBeanMethods = true) // true за замовчуванням
public class DatabaseConfig {

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

    @Bean
    public JdbcTemplate jdbcTemplate() {
        // Виклик dataSource() перехоплюється CGLIB!
        // Замість створення нового HikariDataSource проксі поверне вже створений синглтон із BeanFactory
        return new JdbcTemplate(dataSource());
    }
}

3. Scoped Proxies (@RequestScope, @SessionScope)

Коли бін із життєвим циклом HTTP-запиту впроваджується в Singleton-сервіс, Spring обгортає його в CGLIB-проксі (ScopedProxyFactoryBean). При кожному виклику методу проксі витягує реальний бін поточного потоку з RequestContextHolder.

4. Динамічні репозиторії Spring Data

Коли ви оголошуєте інтерфейс:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

Spring Data не генерує Java-клас на диску. Під час старту контейнера RepositoryFactorySupport створює JDK Dynamic Proxy, який перехоплює виклики методів, генерує JPQL/SQL-запити через QueryLookupStrategy та передає їх у EntityManager.

5. Ліниві ін’єкції (@Lazy)

Якщо над полем впровадження вказана анотація @Lazy:

@Service
public class OrderService {
    @Autowired
    @Lazy
    private HeavyReportService heavyReportService;
}

Spring впроваджує легковажний синтетичний проксі (LazyInitTargetSource). Реальний бін HeavyReportService не буде створений у пам’яті доти, доки не буде викликаний будь-який його метод.


🔴 Senior Level

Хто фізично створює проксі: AnnotationAwareAspectJAutoProxyCreator

Створенням AOP-проксі в контейнері керує спеціалізований BeanPostProcessor — AnnotationAwareAspectJAutoProxyCreator (який успадковує AbstractAutoProxyCreator):

  1. Фаза виконання: метод postProcessAfterInitialization(Object bean, String beanName):
    • Аналізує клас біна та перевіряє всі зареєстровані Advisor (включаючи аспекти @Aspect, TransactionAttributeSourceAdvisor, AsyncAnnotationAdvisor).
    • Якщо знаходиться хоча б один відповідний Pointcut, викликається метод createProxy(...).
    • Залежно від налаштування proxyTargetClass (у Spring Boot за замовчуванням true) фабрика ProxyFactory створює CGLIB або JDK Dynamic Proxy.
  2. Фаза раннього створення (Early Proxy Creation при циклах):
    • Якщо бін бере участь у циклічній залежності (A $\to$ B $\to$ A), проксі створюється раніше стандартної фази ініціалізації — всередині методу SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(bean, beanName).
    • Ранній проксі розміщується у другому рівні кешу earlySingletonObjects.

У чому різниця між AOP-проксі та @Configuration-проксі

Це принципово різні механізми генерації проксі:

Критерій AOP Proxy (@Transactional тощо) Configuration Class Proxy (@Configuration)
Хто створює BeanPostProcessor (AbstractAutoProxyCreator) BeanFactoryPostProcessor (ConfigurationClassPostProcessor)
Коли створюється На етапі ініціалізації бінів (пізній етап) На етапі обробки метаданих BeanDefinition (ранній етап)
Мета проксіювання Впровадити інтерцептори (MethodInterceptor) наскрізної функціональності Перехопити @Bean-методи для повернення одного й того самого синглтона
Технологія CGLIB або JDK Dynamic Proxy Виключно CGLIB (клас конфігурації не має інтерфейсу)
// Спрощена логіка CGLIB-інтерцептора BeanMethodInterceptor у ConfigurationClassEnhancer:
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
    if (isCurrentlyInvokedFactoryMethod(method)) {
        // Прямий виклик створення біна контейнером
        return proxy.invokeSuper(obj, args);
    }
    // Виклик з іншого @Bean-методу: перенаправляємо в BeanFactory!
    return beanFactory.getBean(beanName);
}

Оптимізація та оверхед проксі у високонавантажених системах

Хоча оверхед на виклик методу через проксі становить лічені наносекунди, бездумне створення сотень проксі спричиняє:

  1. Сповільнення холодного старту (Startup Time): рефлексивний пошук Pointcut-виразів та генерація байт-коду CGLIB для тисяч методів збільшують час запуску Spring Boot на десятки секунд.
  2. Споживання Metaspace: кожен динамічно згенерований CGLIB-клас завантажується в Metaspace JVM власним завантажувачем класів.
  3. Особливість для Spring Boot 3 / GraalVM Native Image: при компіляції в Native Image за допомогою AOT (Ahead-Of-Time) динамічна генерація байт-коду CGLIB у рантаймі неможлива. Spring AOT генерує всі проксі-класи статично під час збирання проєкту.

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

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

Spring створює проксі у п’яти основних випадках:

  1. AOP-функціональність: наявність анотацій @Transactional, @Async, @Cacheable, @Validated, @PreAuthorize або кастомних @Aspect. Створюється через AnnotationAwareAspectJAutoProxyCreator на етапі postProcessAfterInitialization.
  2. Конфігураційні класи: класи з @Configuration(proxyBeanMethods = true) проксіюються через CGLIB для перехоплення міжбінових викликів.
  3. Веб-скоупи: біни @RequestScope та @SessionScope обгортаються у Scoped Proxy для безпечного впровадження в синглтони.
  4. Spring Data: інтерфейси репозиторіїв (JpaRepository) реалізуються на льоту через JDK Dynamic Proxy.
  5. Ліниві залежності: ін’єкції з анотацією @Lazy.

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

1. У чому різниця між створенням проксі для @Transactional та для @Configuration?

Відповідь: Проксі для @Transactional створюється компонентом BeanPostProcessor у процесі ініціалізації конкретного екземпляра біна для додавання ланцюжка інтерцепторів. Проксі для @Configuration створюється компонентом BeanFactoryPostProcessor (ConfigurationClassPostProcessor) задовго до створення синглтонів для того, щоб перехоплювати виклики @Bean-методів і повертати зареєстровані синглтони з контейнера.

2. Як працює Spring Data JPA репозиторій, якщо для нього немає класу реалізації?

Відповідь: Spring Data використовує JDK Dynamic Proxy. У момент старту фабрика RepositoryFactorySupport створює динамічний проксі, що реалізує інтерфейс вашого репозиторію. Виклики методів перехоплюються обробником, який парсить ім’я методу або витягує @Query, генерує SQL/JPQL і делегує виконання EntityManager.

3. Коли AOP-проксі може бути створений до фази postProcessAfterInitialization?

Відповідь: У разі циклічної залежності (наприклад, ServiceA залежить від ServiceB, а ServiceB — від ServiceA). Щоб розв’язати цикл, Spring задіює 3-й кеш (singletonFactories), викликаючи SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(), який створює ранній AOP-проксі до стандартної фази ініціалізації.

4. Що відбувається при вказівці @Lazy на полі з @Autowired?

Відповідь: Spring не створює цільовий бін у момент старту застосунку. Замість цього в поле впроваджується легковажний динамічний проксі. Реальний екземпляр біна буде створений у пам’яті лише при першому реальному виклику будь-якого методу через цей проксі.


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

  • ❌ «Spring створює проксі абсолютно для всіх класів з анотацією @Component або @Service» — проксі створюється лише за наявності аспектів, скоупів, @Configuration або репозиторіїв.
  • ❌ «Проксі для @Configuration класів створюється тим самим BeanPostProcessor, що й для @Transactional» — груба помилка. @Configuration обробляється на фазі BeanFactoryPostProcessor.
  • ❌ «Інтерфейси JpaRepository реалізуються через CGLIB» — оскільки це чисті інтерфейси без класів, Spring Data історично використовує JDK Dynamic Proxy.
  • ❌ «Проксіювання @Lazy ініціалізує бін одразу під час старту контексту» — сенс @Lazy саме у відкладанні інстанціювання до першого бойового виклику.

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