Какие типы Proxy существуют
4. Logging / Auditing Proxy (Логирующий): Логирует параметры вызовов, время выполнения и результаты без изменения кода реального класса. 5. Caching Proxy (Кэширующий): Сохраняет...
🟢 Junior Level
Proxy (Заместитель) — это структурный паттерн проектирования, который предоставляет объект-суррогат или точку перехвата, контролирующую доступ к реальному объекту. Заместитель реализует тот же интерфейс, что и целевой объект, позволяя выполнять дополнительные операции до или после передачи вызова оригиналу (или вовсе отменять вызов).
Бытовая аналогия
- Банковская дебетовая карта: Карта выступает прокси-объектом для вашего банковского счета (реального объекта). Магазин взаимодействует с картой, проверяется баланс, блокируется сумма, но физические деньги лежат в хранилище банка.
- Секретарь руководителя: Клиент не заходит сразу в кабинет директора. Секретарь проверяет расписание, аутентифицирует посетителя, фильтрует спам и только при необходимости организует встречу.
Базовые типы Proxy (по классификации GoF):
- Virtual Proxy (Виртуальный): Откладывает создание тяжелого ресурсоемкого объекта до момента его первого фактического использования (ленивая инициализация / Lazy Loading).
- Protection Proxy (Защитный): Проверяет права доступа и авторизацию вызывающего кода перед передачей вызова реальному сервису.
- Remote Proxy (Удаленный): Представляет локальный объект, скрывающий за собой детали сетевого взаимодействия (RPC, gRPC, RMI, HTTP-клиенты).
- Logging / Auditing Proxy (Логирующий): Логирует параметры вызовов, время выполнения и результаты без изменения кода реального класса.
- Caching Proxy (Кэширующий): Сохраняет результаты ресурсоемких операций и отдает кэшированное значение при повторных запросах с теми же аргументами.
Наглядный пример: Виртуальный (ленивый) прокси
// Общий интерфейс для целевого объекта и заместителя
public interface DocumentViewer {
void display();
}
// Реальный тяжелый объект
public class HeavyReport implements DocumentViewer {
private final String filename;
public HeavyReport(String filename) {
this.filename = filename;
loadFromDisk(); // Дорогая операция загрузки и парсинга данных
}
private void loadFromDisk() {
System.out.println("Загрузка тяжелого отчета из диска: " + filename);
}
@Override
public void display() {
System.out.println("Отображение отчета: " + filename);
}
}
// Virtual Proxy: инстанцирует HeavyReport только при реальном вызове display()
public class LazyReportProxy implements DocumentViewer {
private final String filename;
private HeavyReport realReport;
public LazyReportProxy(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realReport == null) {
realReport = new HeavyReport(filename); // Ленивая загрузка
}
realReport.display();
}
}
🟡 Middle Level
Способы создания динамических прокси в Java
В production-разработке прокси редко пишут вручную для каждого класса. Вместо этого используются динамические прокси (Dynamic Proxies), генерируемые на лету во время выполнения (runtime).
1. JDK Dynamic Proxy (java.lang.reflect.Proxy)
Встроенный механизм стандартной библиотеки Java. Генерирует байткод нового класса $Proxy0, реализующего заданные интерфейсы.
- Ограничение: Работает только с интерфейсами. Не может проксировать класс напрямую.
- Ключевые сущности:
Proxy.newProxyInstance(), интерфейсInvocationHandler.
public interface PaymentService {
void pay(double amount);
}
public class PaymentServiceImpl implements PaymentService {
@Override
public void pay(double amount) {
System.out.println("Проведение платежа: " + amount + " USD");
}
}
// Создание динамического JDK прокси
PaymentService target = new PaymentServiceImpl();
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
PaymentService.class.getClassLoader(),
new Class<?>[]{PaymentService.class},
(proxyObj, method, args) -> {
System.out.println("[AUDIT] Старт операции: " + method.getName());
long start = System.currentTimeMillis();
Object result = method.invoke(target, args); // Делегирование
System.out.println("[AUDIT] Завершено за " + (System.currentTimeMillis() - start) + " мс");
return result;
}
);
proxy.pay(150.0);
2. CGLIB (Code Generation Library) / ByteBuddy
Создает прокси путем генерации наследника (сабкласса) целевого класса на уровне байткода.
- Преимущество: Не требует наличия интерфейса, может оборачивать обычные конкретные классы.
- Ограничения:
- Не может переопределять методы, объявленные как
final. - Не может проксировать классы, объявленные как
final. - Требует наличия доступного конструктора для создания сабкласса.
- Не может переопределять методы, объявленные как
Сравнительная таблица: JDK Dynamic Proxy vs CGLIB vs ByteBuddy
| Критерий | JDK Dynamic Proxy | CGLIB | ByteBuddy |
|---|---|---|---|
| Принцип работы | Создает класс-реализацию интерфейса (implements Interface) |
Создает класс-наследник (extends TargetClass) |
Генерация сабклассов или трансформация байткода |
| Необходимость интерфейса | Обязательно | Не требуется | Не требуется |
Работа с final классами/методами |
Не применимо к классам; интерфейсные методы перехватывает | Не перехватывает final-методы, не работает с final-классами |
Не перехватывает final без переопределения через агент |
| Где используется | Spring AOP (до Boot 2.x), MyBatis мапперы | Hibernate прокси, Spring Boot 2.x+ по умолчанию | Mockito 2+, Hibernate 5.3+, современный Spring |
| Поддержка Java модулей (JPMS) | Нативная (встроен в JVM) | Устарела (проблемы с reflective access в Java 17+) | Полная совместимость с современными JDK |
🔴 Senior Level
Прокси в Spring Framework: AOP, Транзакции и Безопасность
Практически вся декларативная функциональность Spring Framework (@Transactional, @Cacheable, @Async, @PreAuthorize, @Retryable) построена на концепции динамических прокси и паттерне Decorator/Interceptor через механизм Spring AOP.
При обращении к бину Spring перехватывает управление через цепочку интерцепторов:
TransactionAspectSupport: открывает транзакцию в БД черезPlatformTransactionManager.- Вызывает метод реального бина (
joinPoint.proceed()). - При успешном завершении фиксирует транзакцию (
commit), приRuntimeException— инициируетrollback.
Эволюция настроек Spring Boot: proxyTargetClass
- До Spring Boot 2.0 (Spring Framework 4.x/5.x):
По умолчанию использовался
JDK Dynamic Proxy, если класс реализовывал хотя бы один интерфейс (proxyTargetClass = false). Если бин не имел интерфейсов, Spring переключался на CGLIB. Это приводило к частым ошибкам внедрения зависимостей по конкретному типу бина (BeanNotOfRequiredTypeException). - Начиная со Spring Boot 2.0+:
Настройка
spring.aop.proxy-target-class=trueустановлена по умолчанию. Spring всегда использует генерацию сабклассов (CGLIB/ByteBuddy), обеспечивая единообразное внедрение зависимостей как по интерфейсу, так и по конкретному типу класса.
Критическая проблема: Self-Invocation (Внутренний вызов)
Классическая архитектурная ловушка на собеседованиях и в production:
@Service
public class OrderService {
public void processOrder(Order order) {
// Бизнес-логика подготовки
this.saveOrderWithTransaction(order); // ❌ ОШИБКА: @Transactional НЕ сработает!
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveOrderWithTransaction(Order order) {
orderRepository.save(order);
}
}
Почему не сработала транзакция?
Вызов this.saveOrderWithTransaction(...) происходит напрямую по ссылке this на экземпляр целевого класса внутри JVM, минуя внешний Spring Proxy. Перехватчик транзакций (TransactionInterceptor) просто не получает управление.
Способы решения проблемы Self-Invocation:
- Архитектурный рефакторинг (Best Practice): Вынести метод во внешний сервис/бин и заинжектить его.
- Self-Injection (Внедрение самого себя):
@Service public class OrderService { @Autowired private OrderService self; // Проксированный экземпляр public void processOrder(Order order) { self.saveOrderWithTransaction(order); // ✅ Вызов идет через прокси! } } AopContext.currentProxy(): Требует явного включения@EnableAspectJAutoProxy(exposeProxy = true):((OrderService) AopContext.currentProxy()).saveOrderWithTransaction(order);- Compile-time / Load-time Weaving (AspectJ): Байткод модифицируется непосредственно компилятором
ajcили загрузчиком классов. В этом случае прокси не создаются, и вызовыthisперехватываются штатно.
Влияние на JVM и Metaspace
- Генерация байткода: Динамические прокси компилируются в runtime и загружаются специальными загрузчиками классов. Описания этих классов размещаются в области памяти Metaspace. В системах с бесконтрольным созданием прокси без кэширования возможна утечка памяти
java.lang.OutOfMemoryError: Metaspace. - JIT-компиляция и девиртуализация: Современный JVM JIT-компилятор (C2) оптимизирует и инлайнит мономорфные вызовы методов через динамические прокси. Однако, если цепочка перехватчиков (Interceptors) слишком глубокая или полиморфная, JIT не может выполнить девиртуализацию вызова, что приводит к дополнительным затратам на вызов метода в ультра-нагруженных сценариях (Hot Path).
🎯 Шпаргалка для интервью
30-секундный ответ (Elevator Pitch)
«Proxy — это структурный паттерн, предоставляющий объект-заместитель для контроля доступа, обогащения или отложенной инициализации реального объекта. Основные типы: Virtual (ленивая загрузка), Protection (проверка прав), Remote (сетевой вызов) и Smart/Interceptor (транзакции, логирование, кэш). В Java различают статические прокси и динамические: JDK Dynamic Proxy (на базе интерфейсов через reflection) и CGLIB/ByteBuddy (генерация сабклассов). В Spring Boot по умолчанию используется проксирование сабклассов (
proxyTargetClass=true), а ключевая ловушка паттерна — Self-invocation, когда вызовthis.method()обходит прокси и отключает аннотации@Transactionalи@Async».
3–4 каверзных вопроса с ответами
- Сработает ли
@Transactionalили@Cacheableна методе с модификаторомprivateилиfinal?- Ответ:
- На
private-методах аннотации Spring AOP не работают, так как прокси (ни JDK, ни CGLIB) не могут переопределить или получить доступ к private-методу сабкласса. - На
final-методах CGLIB не может переопределить метод, вызов уйдет напрямую в целевой объект в обход прокси без создания транзакции. JDK Proxy работает только с интерфейсами, где методы не могут быть final.
- На
- Ответ:
- В чём разница между паттернами Proxy и Decorator?
- Ответ: Оба паттерна имеют идентичную структуру классов, но различаются намерением:
- Proxy контролирует жизненный цикл и доступ к реальному объекту (клиент обычно не создает реальный объект сам, а получает прокси).
- Decorator используется для динамического расширения функциональности и комбинирования поведения, при этом клиент сам собирает композицию из декораторов (
new BufferedInputStream(new FileInputStream(...))).
- Ответ: Оба паттерна имеют идентичную структуру классов, но различаются намерением:
- Что делает Hibernate при вызове
session.load()илиrepository.getReferenceById()?- Ответ: Hibernate возвращает Lazy Proxy (генерируемый через ByteBuddy), содержащий только идентификатор сущности (
ID). Реальный запросSELECTв базу данных не выполняется до тех пор, пока у прокси не будет вызван любой геттер, кромеgetId(). Если сессия закрыта до первого обращения к полям, выбрасываетсяLazyInitializationException.
- Ответ: Hibernate возвращает Lazy Proxy (генерируемый через ByteBuddy), содержащий только идентификатор сущности (
- Как AspectJ решает проблему Self-Invocation в отличие от Spring AOP?
- Ответ: Spring AOP основан на динамических прокси (Runtime Proxying), поэтому вызовы внутри одного объекта идут в обход перехватчика. AspectJ использует модификацию байткода (Weaving) на этапе компиляции (CTW) или загрузки классов агентом (LTW). Код аспекта внедряется непосредственно в тело вызываемого метода, благодаря чему перехватываются любые вызовы, включая
this.method(), конструкторы и приватные методы.
- Ответ: Spring AOP основан на динамических прокси (Runtime Proxying), поэтому вызовы внутри одного объекта идут в обход перехватчика. AspectJ использует модификацию байткода (Weaving) на этапе компиляции (CTW) или загрузки классов агентом (LTW). Код аспекта внедряется непосредственно в тело вызываемого метода, благодаря чему перехватываются любые вызовы, включая
Типичные ошибки и Red Flags
- 🚩 Утверждение, что
@Transactionalгарантированно работает при внутреннем вызовеthis.method()(один из главных маркеров непонимания Spring AOP). - 🚩 Попытка повесить аннотации Spring AOP (
@Transactional,@Async,@Cacheable) наprivateметоды. - 🚩 Утверждение, что JDK Dynamic Proxy быстрее CGLIB во всех случаях (в современных JVM разница минимальна, а CGLIB избавляет от необходимости дублировать методы во всех интерфейсах).
- 🚩 Путаница между Proxy, Decorator и Adapter (Proxy контролирует доступ с тем же интерфейсом; Decorator расширяет поведение с тем же интерфейсом; Adapter адаптирует несовместимый интерфейс к требуемому).
Связанные темы
- В чём преимущество Decorator перед наследованием — детальное сравнение Proxy vs Decorator
- Какие категории паттернов существуют — классификация структурных паттернов GoF
- Что такое паттерны проектирования — введение в паттерны
- Spring AOP и BeanPostProcessor — механизм создания прокси в Spring контейнере
- Как работает транзакционность в Spring — механизм TransactionInterceptor