Вопрос 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