🍃 Раздел 5 · Вопрос #14

Когда Spring создаёт прокси

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 → B → 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 именно в откладывании инстанцирования до первого боевого вызова.

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