Когда Spring создаёт прокси
Spring создаёт динамический прокси не для каждого бина, а только тогда, когда для этого есть явный функциональный триггер.
🟢 Junior Level
Spring создаёт динамический прокси не для каждого бина, а только тогда, когда для этого есть явный функциональный триггер.
Основные триггеры создания прокси
- AOP-аннотации (сквозная функциональность):
@Transactional— управление транзакциями БД.@Async— асинхронное выполнение в отдельном пуле потоков.@Cacheable,@CachePut,@CacheEvict— кэширование возвращаемых значений.@PreAuthorize,@Secured— проверка прав доступа Spring Security.@Validatedна уровне класса сервиса — валидация параметров методов (Hibernate Validator).
- Конфигурационные классы (
@Configuration):- Для перехвата прямых вызовов
@Bean-методов друг из друга с сохранением Singleton-семантики.
- Для перехвата прямых вызовов
- Области видимости (Scoped Proxies):
- При инъекции короткоживущих бинов (
@RequestScope,@SessionScope) в синглтоны.
- При инъекции короткоживущих бинов (
- Интерфейсы Spring Data JPA:
- Для автоматической реализации репозиториев (
JpaRepository,CrudRepository), у которых нет Java-класса с кодом.
- Для автоматической реализации репозиториев (
- Ленивая загрузка зависимостей (
@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):
- Фаза выполнения: метод
postProcessAfterInitialization(Object bean, String beanName):- Анализирует класс бина и проверяет все зарегистрированные
Advisor(включая аспекты@Aspect,TransactionAttributeSourceAdvisor,AsyncAnnotationAdvisor). - Если находится хотя бы один подходящий Pointcut, вызывается метод
createProxy(...). - В зависимости от настройки
proxyTargetClass(в Spring Boot по умолчаниюtrue) фабрикаProxyFactoryсоздает CGLIB или JDK Dynamic Proxy.
- Анализирует класс бина и проверяет все зарегистрированные
- Фаза раннего создания (Early Proxy Creation при циклах):
- Если бин участвует в циклической зависимости (A → B → A), прокси создается раньше фазы инициализации — внутри метода
SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(bean, beanName). - Ранний прокси помещается во второй уровень кэша
earlySingletonObjects.
- Если бин участвует в циклической зависимости (A → B → A), прокси создается раньше фазы инициализации — внутри метода
В чем разница между 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);
}
Оптимизация и оверхед прокси в высоконагруженных системах
Хотя оверхед на вызов метода через прокси составляет единицы наносекунд, бездумное создание сотен прокси влечет:
- Замедление времени холодного старта (Startup Time): рефлексивный поиск Pointcut-выражений и генерация байткода CGLIB для тысяч методов увеличивают время запуска Spring Boot на десятки секунд.
- Потребление Metaspace: каждый динамически сгенерированный CGLIB-класс загружается в Metaspace JVM собственным загрузчиком классов.
- Совет для Spring Boot 3 / GraalVM Native Image: при компиляции в Native Image с помощью AOT (Ahead-Of-Time) динамическая генерация байткода CGLIB в рантайме невозможна. Spring AOT генерирует все прокси-классы статически во время сборки проекта.
🎯 Шпаргалка для интервью
30-секундный ответ
Spring создаёт прокси в пяти основных случаях:
- AOP-функциональность: наличие аннотаций
@Transactional,@Async,@Cacheable,@Validated,@PreAuthorizeили кастомных@Aspect. Создается черезAnnotationAwareAspectJAutoProxyCreatorна этапеpostProcessAfterInitialization.- Конфигурационные классы: классы с
@Configuration(proxyBeanMethods = true)проксируются через CGLIB для перехвата межбиновых вызовов.- Веб-скоупы: бины
@RequestScopeи@SessionScopeоборачиваются в Scoped Proxy для безопасного внедрения в синглтоны.- Spring Data: интерфейсы репозиториев (
JpaRepository) реализуются на лету через JDK Dynamic Proxy.- Ленивые зависимости: инъекции с аннотацией
@Lazy.
4 каверзных вопроса с ответами
-
В чём разница между созданием прокси для
@Transactionalи для@Configuration? Ответ: Прокси для@Transactionalсоздается компонентомBeanPostProcessorв процессе инициализации конкретного экземпляра бина для добавления цепочки интерцепторов. Прокси для@Configurationсоздается компонентомBeanFactoryPostProcessor(ConfigurationClassPostProcessor) задолго до создания синглтонов для того, чтобы перехватывать вызовы@Bean-методов и возвращать зарегистрированные синглтоны из контейнера. -
Как работает Spring Data JPA репозиторий, если для него нет класса реализации? Ответ: Spring Data использует JDK Dynamic Proxy. В момент старта фабрика
RepositoryFactorySupportсоздает динамический прокси, реализующий интерфейс вашего репозитория. Вызовы методов перехватываются обработчиком, который парсит имя метода или извлекает@Query, генерирует SQL/JPQL и делегирует выполнениеEntityManager. -
Когда AOP-прокси может быть создан до фазы
postProcessAfterInitialization? Ответ: В случае циклической зависимости (например, ServiceA зависит от ServiceB, а ServiceB — от ServiceA). Чтобы разрешить цикл, Spring задействует 3-й кэш (singletonFactories), вызываяSmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(), который создает ранний AOP-прокси до стандартной фазы инициализации. -
Что происходит при указании
@Lazyна поле с@Autowired? Ответ: Spring не создает целевой бин в момент старта приложения. Вместо этого в поле внедряется легковесный динамический прокси. Реальный экземпляр бина будет создан в памяти только при первом реальном вызове любого метода через этот прокси.
Красные флаги (чего категорически нельзя говорить)
- ❌ «Spring создаёт прокси абсолютно для всех классов с аннотацией
@Componentили@Service» — прокси создается только при наличии аспектов, скоупов,@Configurationили репозиториев. - ❌ «Прокси для
@Configurationклассов создается тем жеBeanPostProcessor, что и для@Transactional» — грубая ошибка.@Configurationобрабатывается на фазеBeanFactoryPostProcessor. - ❌ «Интерфейсы
JpaRepositoryреализуются через CGLIB» — так как это чистые интерфейсы без классов, Spring Data исторически использует JDK Dynamic Proxy. - ❌ «Проксирование
@Lazyинициализирует бин сразу при старте контекста» — смысл@Lazyименно в откладывании инстанцирования до первого боевого вызова.