Что такое прокси в 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:
- Вызов метода перехватывается сгенерированным прокси-объектом.
- Прокси запрашивает у фабрики цепочку перехватчиков (
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 каверзных вопроса с ответами
-
Почему в Spring Boot по умолчанию включен CGLIB (
proxy-target-class=true), а не JDK Dynamic Proxy? Ответ: Чтобы избежать ошибок несоответствия типов (BeanNotOfRequiredTypeException). При использовании JDK-прокси объект реализует только интерфейсы и не является экземпляром конкретного класса. Поэтому инъекция по типу реализации (@Autowired MyServiceImpl) падает. CGLIB создает подкласс целевого класса, что позволяет инжектировать зависимость как по интерфейсу, так и по классу. -
Что произойдет, если повесить
@Transactionalнаfinal-метод в сервисе на Spring Boot? Ответ: Spring Boot использует CGLIB, который переопределяет методы в подклассе. Так как методfinal, переопределить его невозможно. В рантайме вызов метода пройдет напрямую в оригинальный объект, минуя интерцептор транзакций. Транзакция не откроется, при этом никаких исключений выброшено не будет (тихий баг). -
Почему Spring AOP не перехватывает
private-методы? Ответ: Spring AOP основан на динамическом проксировании во время выполнения. JDK Proxy реализует интерфейсы (в которых нет приватных методов), а CGLIB наследует класс и не имеет видимости приватных методов суперкласса для их переопределения. Если требуется перехватprivate-методов, необходимо использовать полноценный AspectJ (Compile-Time / Load-Time Weaving). -
Как определить, что бин является прокси-объектом, и как получить его реальный класс? Ответ: С помощью утилиты
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 избавляет от проблем типизации при внедрении.