Що таке proxy в Spring
Проксі дозволяє реалізувати концепцію AOP (Aspect-Oriented Programming) — виносити наскрізну функціональність (Cross-Cutting Concerns) за межі чистої бізнес-логіки:
🟢 Junior Level
Проксі (Proxy) у Spring — це згенерований у рантаймі об’єкт-обгортка (делегат), який перехоплює виклики методів цільового біна (Target Object), виконує наскрізну системну функціональність (відкриття транзакцій, логування, перевірка прав доступу, кешування) і лише потім передає керування реальному об’єкту.
Навіщо потрібен Proxy
Проксі дозволяє реалізувати концепцію AOP (Aspect-Oriented Programming) — виносити наскрізну функціональність (Cross-Cutting Concerns) за межі чистої бізнес-логіки:
@Transactional— відкриває транзакцію, фіксує її (commit) або відкочує (rollback).@Async— запускає метод в окремому потоці з пулу задач.@Cacheable— перевіряє наявність даних у кеші до виконання методу.@Secured/@PreAuthorize— перевіряє ролі та права поточного користувача перед виконанням.
Клієнтський код (Controller)
↓
[ Dynamic Proxy (Обгортка) ] ──> Перевірка ролей, відкриття транзакції
↓
[ Target Object (Ваш Service) ] ──> Виконання бізнес-логіки
↓
[ Dynamic Proxy (Обгортка) ] ──> Фіксація (commit) транзакції, логування
↓
Повернення результату клієнту
Приклад із практики
package com.example.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class OrderService {
@Transactional
public void createOrder(Long userId, Long itemId) {
// Бізнес-логіка створення замовлення
System.out.println("Виконання методу createOrder у реальному об'єкті");
}
}
Коли інший компонент впроваджує OrderService:
@Autowired
private OrderService orderService; // Spring впроваджує не напряму OrderService, а згенерований PROXY!
🟡 Middle Level
Два механізми створення проксі у Spring
Spring підтримує дві технології динамічного проксіювання:
┌───────────────────────────────────────────────┐
│ Як Spring створює Проксі? │
└───────────────────────┬───────────────────────┘
│
┌───────────────────────┴───────────────────────┐
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ JDK Dynamic Proxy │ │ CGLIB Proxy │
├────────────────────────┤ ├────────────────────────┤
│ • Стандарт Java │ │ • Стороння бібліотека │
│ • Лише інтерфейси │ │ • Через успадкування │
│ • Реалізує інтерфейс │ │ • Створює клас-нащадок │
└────────────────────────┘ └────────────────────────┘
| Характеристика | JDK Dynamic Proxy | CGLIB Proxy |
|---|---|---|
| Базова технологія | java.lang.reflect.Proxy (частина JDK) |
Репакетований CGLIB / ByteBuddy |
| Принцип роботи | Генерує клас, що реалізує ті самі інтерфейси | Генерує підклас-нащадок цільового класу |
| Вимога до цільового класу | Зобов’язаний реалізовувати хоча б один інтерфейс | Клас і методи не можуть бути final |
| Ін’єкція за типом класу | ❌ Неможлива (впаде з помилкою типів) | ✅ Можлива (проксі є нащадком класу) |
| Ім’я згенерованого класу | jdk.proxy2.$Proxy123 |
OrderService$$SpringCGLIB$$0 |
Налаштування за замовчуванням: чому Spring Boot обрав CGLIB
Починаючи зі Spring Boot 2.0+ (і в Spring Boot 3.x), властивість spring.aop.proxy-target-class за замовчуванням виставлена в true.
Чому CGLIB став стандартом де-факто: У старих версіях Spring (де за замовчуванням використовувався JDK Dynamic Proxy), якщо сервіс реалізовував інтерфейс:
public interface OrderService { void process(); }
@Service
public class OrderServiceImpl implements OrderService { ... }
Спроба впровадити сервіс за конкретним типом класу реалізації:
@Autowired
private OrderServiceImpl orderService; // ПОМИЛКА: BeanNotOfRequiredTypeException!
призводила до аварійної зупинки застосунку, оскільки JDK-проксі реалізовував OrderService, але не був нащадком OrderServiceImpl. CGLIB усунув цю проблему, створюючи підклас OrderServiceImpl, що дозволяє інжектувати залежність як за інтерфейсом, так і за класом.
🔴 Senior Level
Внутрішня архітектура перехоплення (Interceptor Chain)
Усередині Spring AOP проксі є реалізацією інтерфейсу Advised:
- Виклик методу перехоплюється згенерованим проксі-об’єктом.
- Проксі запитує у фабрики ланцюжок перехоплювачів (
List<MethodInterceptor>), які застосовні до цього методу (на основі Pointcut-виразів). - Створюється об’єкт виклику
ReflectiveMethodInvocation. - Метод
proceed()по черзі викликає кожен перехоплювач у ланцюжку (Chain of Responsibility):
MethodInvocation.proceed():
1. SecurityInterceptor (перевірка прав)
2. TransactionInterceptor (TransactionManager.getTransaction())
3. CustomAspect (логування / метрики)
4. Method.invoke(target, args) -> виклик вихідного бізнес-коду
5. TransactionInterceptor (TransactionManager.commit())
Обмеження Spring AOP та приховані баги в Production
Оскільки Spring AOP працює через проксі часу виконання, він має фундаментальні архітектурні обмеження:
1. Тихий баг із final-методами у CGLIB
CGLIB генерує клас-нащадок і перевизначає методи. У Java методи з модифікатором final не можуть бути перевизначені.
@Service
public class BillingService {
@Transactional
public final void chargeCustomer(Long id) {
// ПОМИЛКА: Spring Boot використовує CGLIB.
// Метод final НЕ може бути перевизначений проксі!
// Виклик піде НАПРЯМУ в BillingService в обхід TransactionInterceptor!
}
}
Увага: Компілятор не видасть помилки, Spring успішно запуститься, але метод виконуватиметься БЕЗ транзакції!
2. Проблема Self-Invocation (виклик усередині одного класу)
Коли один метод класу викликає інший метод того самого класу через this:
@Service
public class OrderService {
public void processOrder() {
// Виклик this.saveToDatabase() оминає проксі!
saveToDatabase();
}
@Transactional
public void saveToDatabase() {
// Транзакція НЕ відкриється!
}
}
Ключове слово this посилається на реальний цільовий об’єкт (Target), а не на Proxy. Виклик оминає ланцюжок інтерцепторів.
3. private-методи
Spring AOP не може проксіювати private-методи:
- JDK Proxy проксіює лише методи інтерфейсів (де всі методи публічні).
- CGLIB не може перевизначити
private-методи батьківського класу. - Анотації
@Transactional,@Asyncта@Cacheableнаprivate-методах ігноруються.
Програмна інспекція проксі в коді
Для роботи з проксі-об’єктами Spring надає утилітарні класи AopUtils та AopProxyUtils:
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.framework.AopProxyUtils;
public void inspectBean(Object bean) {
boolean isProxy = AopUtils.isAopProxy(bean); // true, якщо об'єкт є будь-яким AOP-проксі
boolean isCglib = AopUtils.isCglibProxy(bean); // true, якщо CGLIB
boolean isJdk = AopUtils.isJdkDynamicProxy(bean); // true, якщо JDK Dynamic Proxy
// Отримати реальний базовий клас сутності без проксі-обгорток
Class<?> targetClass = AopUtils.getTargetClass(bean);
// Отримати вихідний сирий об'єкт (unproxied target)
Object targetObject = AopProxyUtils.getSingletonTarget(bean);
}
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
Проксі у Spring — це об’єкт-посередник, що обгортає бін для декларативного впровадження наскрізної функціональності (AOP: транзакції, кешування, асинхронність, безпека). У Spring є два типи проксі:
- JDK Dynamic Proxy: генерується на основі інтерфейсів через
java.lang.reflect.Proxy.- CGLIB Proxy: генерується на основі створення підкласу (успадкування). У Spring Boot 2.x/3.x за замовчуванням використовується CGLIB (
proxy-target-class=true). Проксі перехоплює виклики через ланцюжокMethodInterceptor. Не працює дляprivate-методів,final-методів (у CGLIB) та при внутрішніх викликах (self-invocation).
4 каверзних питання з відповідями
1. Чому в Spring Boot за замовчуванням увімкнено CGLIB (proxy-target-class=true), а не JDK Dynamic Proxy?
Відповідь: Щоб уникнути помилок невідповідності типів (BeanNotOfRequiredTypeException). При використанні JDK-проксі об’єкт реалізує лише інтерфейси і не є екземпляром конкретного класу. Тому ін’єкція за типом реалізації (@Autowired MyServiceImpl) падає. CGLIB створює підклас цільового класу, що дозволяє інжектувати залежність як за інтерфейсом, так і за класом.
2. Що станеться, якщо розмістити @Transactional на final-методі в сервісі на Spring Boot?
Відповідь: Spring Boot використовує CGLIB, який перевизначає методи в підкласі. Оскільки метод оголошено як final, перевизначити його неможливо. У рантаймі виклик методу піде напряму в оригінальний об’єкт, оминаючи інтерцептор транзакцій. Транзакція не відкриється, при цьому жодних винятків викинуто не буде (тихий дефект).
3. Чому Spring AOP не перехоплює private-методи?
Відповідь: Spring AOP базується на динамічному проксіюванні під час виконання. JDK Proxy реалізує інтерфейси (у яких немає приватних методів), а CGLIB успадковує клас і не бачить приватних методів суперкласу для їх перевизначення. Якщо потрібне перехоплення private-методів, необхідно використовувати повноцінний AspectJ (Compile-Time / Load-Time Weaving).
4. Як визначити, що бін є проксі-об’єктом, і як отримати його реальний клас?
Відповідь: За допомогою утиліти AopUtils.isAopProxy(bean). Щоб дізнатися реальний клас цільового об’єкта без суфіксів $$SpringCGLIB$$, використовується AopUtils.getTargetClass(bean).
Червоні прапорці (чого категорично не можна говорити)
- ❌ «Spring обгортає в проксі абсолютно кожен створений бін» — проксі створюються лише для бінів, що підпадають під Pointcut аспектів (
@Transactional,@Async, кастомні@Aspect). - ❌ «CGLIB без проблем проксіює
final-класи та методи» — у Java успадкування відfinal-класів та перевизначенняfinal-методів заборонено JVM на рівні байт-коду. - ❌ «Self-invocation можна вирішити переведенням проєкту з CGLIB на JDK Proxy» — self-invocation ламає перехоплення в обох типах проксі, оскільки
thisзавжди вказує на цільовий об’єкт, а не на проксі. - ❌ «JDK Dynamic Proxy працює швидше за CGLIB, тому йому завжди треба надавати перевагу» — у сучасних версіях Java/ByteBuddy різниця у швидкості виклику нівелюється JIT-компілятором, а CGLIB позбавляє від проблем типізації при впровадженні.