Питання 14 · Розділ 2

Які типи Proxy існують?

Рішення: (1) self-injection: @Autowired private OrderService self; self.create(), (2) AopContext.currentProxy(), (3) рефакторинг — винести в окремий бін.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

Proxy — об’єкт-замісник, який контролює доступ до іншого об’єкта.

Проста аналогія: Секретар не робить роботу директора, але контролює, хто може до нього потрапити.

Приклад:

// Реальний об'єкт
interface Image { void display(); }

class RealImage implements Image {
    private String filename;

    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();  // Дорога операція!
    }

    private void loadFromDisk() {
        System.out.println("Loading: " + filename);
    }

    public void display() {
        System.out.println("Displaying: " + filename);
    }
}

// Proxy — завантажує тільки коли потрібно
class ProxyImage implements Image {
    private String filename;
    private RealImage realImage;  // Ще не створено!

    public ProxyImage(String filename) {
        this.filename = filename;
    }

    public void display() {
        // Ледаче завантаження
        if (realImage == null) {
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

// Використання
Image image = new ProxyImage("photo.jpg");  // Швидко
image.display();  // Тільки тут завантажується!

🟡 Middle Level

Типи Proxy

1. Virtual Proxy (ледаче завантаження):

// Завантаження тільки за вимогою
class LazyLoader<T> {
    private final Supplier<T> factory;
    private T instance;

    public T get() {
        if (instance == null) {
            instance = factory.get();
        }
        return instance;
    }
}

2. Protection Proxy (контроль доступу):

class ProtectedDocument {
    private final String content;
    private final String owner;

    public void read(String user) {
        if (!user.equals(owner)) {
            throw new SecurityException("Access denied");
        }
        System.out.println(content);
    }
}

3. Remote Proxy (віддалений доступ):

// RMI, gRPC — проксі для віддалених об'єктів
interface RemoteService { String getData(); }

class RemoteServiceProxy implements RemoteService {
    public String getData() {
        // Мережевий виклик до віддаленого сервісу
        return httpClient.get("http://remote-service/data");
    }
}

4. Logging Proxy:

class LoggingProxy implements UserService {
    private final UserService real;
    private final Logger log;

    public User findById(Long id) {
        log.info("findById(" + id + ")");
        return real.findById(id);
    }
}

JDK Dynamic Proxy

// Проксі створюється на runtime
interface BusinessService { void process(); }

class RealService implements BusinessService {
    public void process() { System.out.println("Processing..."); }
}

// Створення проксі
BusinessService proxy = (BusinessService) Proxy.newProxyInstance(
    BusinessService.class.getClassLoader(),
    new Class<?>[] { BusinessService.class },
    (proxyObj, method, args) -> {
        System.out.println("Before: " + method.getName());
        Object result = method.invoke(new RealService(), args);
        System.out.println("After: " + method.getName());
        return result;
    }
);

proxy.process();
// Before: process
// Processing...
// After: process

🔴 Senior Level

JDK Proxy vs CGLIB

У сучасних JDK (8u40+) різниця мінімальна — JDK Proxy використовує MethodHandle/invokedynamic.

Критерій JDK Dynamic Proxy CGLIB
Вимагає Інтерфейс Наслідування
Швидкість створення Швидко Повільно
Швидкість виклику ~дорівнює CGLIB (MethodHandle) ~дорівнює JDK Proxy
final методи Н/Д Не може перевизначити

Spring Proxy вибір

// Spring Framework: JDK Proxy (якщо є інтерфейс).
// Spring Boot 2.x+: proxyTargetClass=true за замовчуванням → CGLIB.
@Service
public class UserServiceImpl implements UserService { }
// → Spring Framework: JDK Dynamic Proxy
// → Spring Boot 2.x+: CGLIB (proxyTargetClass=true за замовчуванням)

// forceAutoProxy = CGLIB
@Service
public class UserService { }  // Без інтерфейсу
// → CGLIB Proxy (наслідування)

@Transactional як Proxy

@Transactional
public void transfer() {
    // Spring огортає метод у:
    // 1. BEGIN TRANSACTION
    // 2. Виклик реального методу
    // 3. COMMIT / ROLLBACK
}

Production Experience

Реальний сценарій: Self-invocation проблема

@Service
public class OrderService {
    @Transactional
    public void create() { /* ... */ }

    public void createBatch() {
        for (int i = 0; i < 10; i++) {
            this.create();  // ❌ Proxy не спрацює!
        }
    }
}

Рішення: (1) self-injection: @Autowired private OrderService self; self.create(), (2) AopContext.currentProxy(), (3) рефакторинг — винести в окремий бін.

Best Practices

  1. JDK Proxy якщо є інтерфейс
  2. CGLIB якщо немає інтерфейсу
  3. Уникайте self-invocation
  4. Proxy overhead у Hot-path

Резюме для Senior

  • JDK Proxy = інтерфейси, у сучасних JDK швидкість ~дорівнює CGLIB (MethodHandle)
  • CGLIB = наслідування
  • Spring = автоматично обирає тип (Boot 2.x+: CGLIB за замовчуванням)
  • Self-invocation = proxy не працює

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

Обов’язково знати:

  • Proxy — об’єкт-замісник, що контролює доступ до іншого об’єкта (логування, кеш, безпека, lazy loading)
  • JDK Dynamic Proxy вимагає інтерфейс, CGLIB працює через наслідування (не може перевизначити final методи)
  • Spring Framework: JDK Proxy якщо є інтерфейс; Spring Boot 2.x+: CGLIB за замовчуванням (proxyTargetClass=true)
  • @Transactional, @Cacheable, @Async — все це Proxy під капотом
  • Self-invocation проблема: виклик this.method() всередині біна не проходить через Proxy
  • Virtual Proxy — ледаче завантаження, Protection Proxy — контроль доступу, Remote Proxy — віддалений доступ
  • У сучасних JDK різниця у швидкості JDK Proxy vs CGLIB мінімальна (MethodHandle/invokedynamic)

Часті уточнювальні запитання:

  • Чому @Transactional не працює при виклику this? — this пропускає Proxy, потрібен self-injection або AopContext.currentProxy()
  • Коли Spring обере CGLIB замість JDK Proxy? — Коли немає інтерфейсу або proxyTargetClass=true (Boot 2.x+)
  • Чим Proxy відрізняється від Decorator? — Proxy контролює доступ до об’єкта, Decorator додає функціональність
  • Який overhead у Proxy? — Додатковий рівень опосередкування, у сучасних JDK мінімальний (MethodHandle)

Червоні прапорці (НЕ говорити):

  • “Proxy завжди повільніший за прямий виклик” — у сучасних JDK різниця мінімальна
  • “Я можу використовувати @Transactional на private методі” — Proxy не працює на private методах
  • “this.create() викличе @Transactional” — self-invocation проблема, Proxy не спрацює
  • “CGLIB завжди швидший за JDK Proxy” — у сучасних JDK швидкість приблизно однакова

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

  • [[12. В чому перевага Decorator перед наслідуванням]] — Proxy vs Decorator
  • [[11. Як Observer реалізований в Java]] — Spring Events через Proxy
  • [[2. Які категорії патернів існують]] — Structural патерни
  • [[16. Які антипатерни ви знаєте]] — Poltergeist (зайвий рівень опосередкування)
  • [[1. Що таке патерни проектування]] — Proxy у Spring AOP