Вопрос 13 · Раздел 5

Что такое прокси в Spring?

4. AopUtils.getTargetClass() → для рефлексии 5. Избегайте final на проксируемых методах 6. Spring Boot → CGLIB по умолчанию

Версии по языкам: English Russian Ukrainian

🟢 Junior Level

Прокси — объект-посредник, который Spring создаёт вокруг вашего бина.

Простая аналогия: Секретарь. Вы хотите поговорить с директором, но сначала проходите через секретаря, который проверяет ваши документы, записывает на приём и т.д.

@Service
public class UserService {
    @Transactional  // ← Spring создаст Proxy вокруг этого бина
    public void save(User user) { }
}

// Вы получаете не UserService, а Proxy!
// Proxy → открывает транзакцию → вызывает save() → закрывает транзакцию

Зачем:

  • Добавляет функциональность без изменения вашего кода
  • @Transactional, @Async, @Cacheable — всё через прокси

🟡 Middle Level

Два типа прокси

JDK Dynamic Proxy:

// Через интерфейсы
public interface UserService { void save(User user); }

// Spring создаёт:
class UserServiceProxy implements UserService {
    private UserService target;
    public void save(User user) {
        // До вызова
        target.save(user);
        // После вызова
    }
}

CGLIB:

// Через наследование
class UserServiceProxy extends UserService {
    // Переопределяет методы
}

// ❌ Не может проксировать final классы/методы!

Spring Boot по умолчанию

Spring Boot 2.x/3.x: proxy-target-class=true по умолчанию, поэтому CGLIB используется всегда, даже если есть интерфейсы. Spring 1.x: JDK Dynamic Proxy по умолчанию.

Почему:
  → Меньше проблем с типами
  → Можно инжектить по классу, не по интерфейсу

Само-вызов (Self-invocation)

@Service
public class UserService {
    public void outer() {
        inner();  // ❌ @Transactional НЕ сработает!
    }
    
    @Transactional
    public void inner() { }
}

// outer() → inner() идёт напрямую, мимо прокси!

🔴 Senior Level

Цепочка интерцепторов

Вызов метода на прокси:
  1. SecurityInterceptor → проверка прав
  2. TransactionInterceptor → открытие транзакции
  3. CacheInterceptor → проверка кэша
  4. Target method → ваш код
  5. Commit/Rollback
  6. Возврат результата
  
→ Каждый интерцептор = один Advice
→ Порядок определяется @Order

Как проверить наличие прокси

// В дебаггере:
// JDK: com.sun.proxy.$Proxy123
// CGLIB: UserService$$EnhancerBySpringCGLIB$$abc123

// Программно:
AopUtils.isAopProxy(bean);
AopUtils.isCglibProxy(bean);
AopUtils.getTargetClass(proxy);  // когда вы работаете с объектом, но не знаете, прокси это или оригинал (например, при сравнении типов через `instanceof` или `getDeclaredMethod`).

Production Experience

Реальный сценарий: final метод не проксируется

@Service
public class UserService {
    @Transactional
    public final void save(User user) { }  // final!
}

// CGLIB создаёт подкласс и переопределяет методы.
// `final` методы переопределить нельзя, поэтому вызов идёт
// напрямую — мимо логики прокси (транзакций, логирования и т.д.)

// Транзакция НЕ создаётся!
// → Данные сохраняются БЕЗ транзакции
// → Баг в production!

Best Practices

  1. JDK Proxy → через интерфейсы
  2. CGLIB → через наследование (не final!)
  3. Self-invocation → транзакция не работает
  4. AopUtils.getTargetClass() → для рефлексии
  5. Избегайте final на проксируемых методах
  6. Spring Boot → CGLIB по умолчанию

Резюме для Senior

  • Прокси = обёртка для AOP
  • JDK = интерфейсы, CGLIB = наследование
  • Spring Boot → CGLIB всегда
  • Self-invocation = вызов мимо прокси
  • final методы → не проксируются CGLIB
  • Цепочка интерцепторов → порядок через @Order

🎯 Шпаргалка для интервью

Обязательно знать:

  • Прокси = объект-обёртка, добавляющий AOP-функциональность без изменения кода
  • Два типа: JDK Dynamic Proxy (через интерфейсы) и CGLIB (через наследование)
  • Spring Boot 2.x/3.x: proxy-target-class=true по умолчанию → всегда CGLIB
  • @Transactional, @Async, @Cacheable — все работают через прокси
  • Self-invocation: вызов this.method() идёт напрямую, минуя прокси
  • final-классы и final-методы НЕ могут быть проксированы через CGLIB
  • Цепочка интерцепторов: Security → Transaction → Cache → Target method

Частые уточняющие вопросы:

  • Как проверить, является ли бин прокси? → AopUtils.isAopProxy(bean), AopUtils.isCglibProxy(bean)
  • Почему self-invocation не работает? → this ссылается на Target, а не на Proxy
  • Как получить оригинальный класс из прокси? → AopUtils.getTargetClass(proxy)
  • В чём разница JDK Proxy и CGLIB? → JDK требует интерфейс, CGLIB создаёт подкласс

Красные флаги (НЕ говорить):

  • «CGLIB может проксировать final-методы» — не может, наследование невозможно
  • «Self-invocation работает с CGLIB» — не работает, this = Target
  • «Прокси добавляет значительный overhead» — наносекунды, пренебрежимо
  • «Spring создаёт прокси для каждого бина» — только с AOP-аннотациями

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

  • [[14. Когда Spring создаёт прокси]]
  • [[15. Что такое AOP (Aspect-Oriented Programming)]]
  • [[18. Почему @Transactional не работает при self-invocation]]
  • [[19. Как решить проблему с self-invocation]]