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

Что такое прокси в 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 (Обёртка) ] ──> Коммит транзакции, логирование
             ↓
Возврат результата клиенту

Пример из практики

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:

  1. Вызов метода перехватывается сгенерированным прокси-объектом.
  2. Прокси запрашивает у фабрики цепочку перехватчиков (List<MethodInterceptor>), применимых к данному методу (на основе Pointcut-выражений).
  3. Создается объект вызова ReflectiveMethodInvocation.
  4. Метод 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 есть два типа прокси:

  1. JDK Dynamic Proxy: генерируется на основе интерфейсов через java.lang.reflect.Proxy.
  2. 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 избавляет от проблем типизации при внедрении.

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