🧩 Розділ 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

У промисловій розробці проксі рідко пишуть вручну під кожен клас. Замість цього використовуються динамічні проксі (Dynamic Proxies), які генеруються на льоту під час виконання програми (runtime).

1. JDK Dynamic Proxy (java.lang.reflect.Proxy)

Вбудований механізм стандартної бібліотеки Java. Генерує байткод нового класу $Proxy0, що реалізує вказані інтерфейси.

  • Обмеження: Працює виключно з інтерфейсами. Не може проксувати клас безпосередньо.
  • Ключові класи: Proxy.newProxyInstance(), інтерфейс InvocationHandler.
import java.lang.reflect.Proxy;

public interface PaymentService {
    void pay(double amount);
}

public class PaymentServiceImpl implements PaymentService {
    @Override
    public void pay(double amount) {
        System.out.println("Проведення платежу: " + amount + " UAH");
    }
}

// Створення динамічного 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(250.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 без перевизначення через Java Agent
Де використовується Spring AOP (до Boot 2.x), мапери MyBatis Hibernate-проксі, Spring Boot 2.x+ за замовчуванням Mockito 2+, Hibernate 5.3+, сучасний Spring
Підтримка модулів (JPMS) Нативна (вбудовано в JVM) Застаріла (проблеми з reflective access у Java 17+) Повна сумісність із сучасними версіями JDK

🔴 Senior Level

Проксі у Spring Framework: AOP, Транзакції та Безпека

Практично вся декларативна магія Spring Framework (@Transactional, @Cacheable, @Async, @PreAuthorize, @Retryable) побудована на динамічних проксі та патерні Decorator/Interceptor через механізм Spring AOP.

Під час звернення до біна Spring перехоплює управління через ланцюжок перехоплювачів (Interceptors):

  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 (Внутрішній виклик)

Класичне питання на Senior-співбесідах та джерело серйозних багів у продакшені:

@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 або завантажувачем класів через Java Agent. У такому разі проксі не створюються взагалі, і виклики this перехоплюються коректно.

Вплив на JVM та пам’ять Metaspace

  1. Динамічна генерація байткоду: Проксі компілюються в runtime та завантажуються спеціальними завантажувачами класів. Метадані цих класів зберігаються в області пам’яті Metaspace. У застосунках із неконтрольованою генерацією унікальних проксі без кешування можливий критичний витік пам’яті java.lang.OutOfMemoryError: Metaspace.
  2. JIT-компіляція та девіртуалізація: Сучасний JVM JIT-компілятор (C2) успішно оптимізує та інлайнить мономорфні виклики через проксі. Проте за наявності глибоких ланцюжків поліморфних інтерцепторів JIT вимикає девіртуалізацію, що спричиняє додаткові накладні витрати на виклик методів у highload-сценаріях.

🎯 Шпаргалка для інтерв’ю

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). Реальний SQL-запит SELECT до бази даних не надсилається доти, доки у проксі не буде викликано будь-який гетер, крім getId(). Якщо сесію EntityManager закрито до першого звернення до полів, викидається LazyInitializationException.
  4. Як AspectJ вирішує проблему Self-Invocation на відміну від Spring AOP?
    • Відповідь: Spring AOP базується на динамічних проксі (Runtime Proxying), тому виклики через this всередині класу оминають перехоплювач. 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 узгоджує несумісний інтерфейс із цільовим).

Пов’язані теми