🧩 Раздел 2 · Вопрос #14

Какие типы Proxy существуют

4. Logging / Auditing Proxy (Логирующий): Логирует параметры вызовов, время выполнения и результаты без изменения кода реального класса. 5. Caching Proxy (Кэширующий): Сохраняет...


🟢 Junior Level

Proxy (Заместитель) — это структурный паттерн проектирования, который предоставляет объект-суррогат или точку перехвата, контролирующую доступ к реальному объекту. Заместитель реализует тот же интерфейс, что и целевой объект, позволяя выполнять дополнительные операции до или после передачи вызова оригиналу (или вовсе отменять вызов).


Бытовая аналогия

  • Банковская дебетовая карта: Карта выступает прокси-объектом для вашего банковского счета (реального объекта). Магазин взаимодействует с картой, проверяется баланс, блокируется сумма, но физические деньги лежат в хранилище банка.
  • Секретарь руководителя: Клиент не заходит сразу в кабинет директора. Секретарь проверяет расписание, аутентифицирует посетителя, фильтрует спам и только при необходимости организует встречу.

Базовые типы Proxy (по классификации GoF):

  1. Virtual Proxy (Виртуальный): Откладывает создание тяжелого ресурсоемкого объекта до момента его первого фактического использования (ленивая инициализация / Lazy Loading).
  2. Protection Proxy (Защитный): Проверяет права доступа и авторизацию вызывающего кода перед передачей вызова реальному сервису.
  3. Remote Proxy (Удаленный): Представляет локальный объект, скрывающий за собой детали сетевого взаимодействия (RPC, gRPC, RMI, HTTP-клиенты).
  4. Logging / Auditing Proxy (Логирующий): Логирует параметры вызовов, время выполнения и результаты без изменения кода реального класса.
  5. 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 перехватывает управление через цепочку интерцепторов:

  1. TransactionAspectSupport: открывает транзакцию в БД через PlatformTransactionManager.
  2. Вызывает метод реального бина (joinPoint.proceed()).
  3. При успешном завершении фиксирует транзакцию (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:

  1. Архитектурный рефакторинг (Best Practice): Вынести метод во внешний сервис/бин и заинжектить его.
  2. Self-Injection (Внедрение самого себя):
    @Service
    public class OrderService {
        @Autowired
        private OrderService self; // Проксированный экземпляр
           
        public void processOrder(Order order) {
            self.saveOrderWithTransaction(order); // ✅ Вызов идет через прокси!
        }
    }
    
  3. AopContext.currentProxy(): Требует явного включения @EnableAspectJAutoProxy(exposeProxy = true):
    ((OrderService) AopContext.currentProxy()).saveOrderWithTransaction(order);
    
  4. Compile-time / Load-time Weaving (AspectJ): Байткод модифицируется непосредственно компилятором ajc или загрузчиком классов. В этом случае прокси не создаются, и вызовы this перехватываются штатно.

Влияние на JVM и Metaspace

  1. Генерация байткода: Динамические прокси компилируются в runtime и загружаются специальными загрузчиками классов. Описания этих классов размещаются в области памяти Metaspace. В системах с бесконтрольным созданием прокси без кэширования возможна утечка памяти java.lang.OutOfMemoryError: Metaspace.
  2. 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 каверзных вопроса с ответами

  1. Сработает ли @Transactional или @Cacheable на методе с модификатором private или final?
    • Ответ:
      • На private-методах аннотации Spring AOP не работают, так как прокси (ни JDK, ни CGLIB) не могут переопределить или получить доступ к private-методу сабкласса.
      • На final-методах CGLIB не может переопределить метод, вызов уйдет напрямую в целевой объект в обход прокси без создания транзакции. JDK Proxy работает только с интерфейсами, где методы не могут быть final.
  2. В чём разница между паттернами Proxy и Decorator?
    • Ответ: Оба паттерна имеют идентичную структуру классов, но различаются намерением:
      • Proxy контролирует жизненный цикл и доступ к реальному объекту (клиент обычно не создает реальный объект сам, а получает прокси).
      • Decorator используется для динамического расширения функциональности и комбинирования поведения, при этом клиент сам собирает композицию из декораторов (new BufferedInputStream(new FileInputStream(...))).
  3. Что делает Hibernate при вызове session.load() или repository.getReferenceById()?
    • Ответ: Hibernate возвращает Lazy Proxy (генерируемый через ByteBuddy), содержащий только идентификатор сущности (ID). Реальный запрос SELECT в базу данных не выполняется до тех пор, пока у прокси не будет вызван любой геттер, кроме getId(). Если сессия закрыта до первого обращения к полям, выбрасывается LazyInitializationException.
  4. Как AspectJ решает проблему Self-Invocation в отличие от Spring AOP?
    • Ответ: Spring AOP основан на динамических прокси (Runtime Proxying), поэтому вызовы внутри одного объекта идут в обход перехватчика. AspectJ использует модификацию байткода (Weaving) на этапе компиляции (CTW) или загрузки классов агентом (LTW). Код аспекта внедряется непосредственно в тело вызываемого метода, благодаря чему перехватываются любые вызовы, включая this.method(), конструкторы и приватные методы.

Типичные ошибки и 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 адаптирует несовместимый интерфейс к требуемому).

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