Коли Spring створює proxy
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 $\to$ B $\to$ A), проксі створюється раніше стандартної фази ініціалізації — всередині методу
SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference(bean, beanName). - Ранній проксі розміщується у другому рівні кешу
earlySingletonObjects.
- Якщо бін бере участь у циклічній залежності (A $\to$ B $\to$ 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 каверзних питання з відповідями
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саме у відкладанні інстанціювання до першого бойового виклику.