JavaRush /Курсы /Spring Core /JDK и class-based proxy

JDK и class-based proxy

Spring Core
21 уровень , 1 лекция
Открыта

1. Два вида proxy в Spring

Когда новичок слышит “proxy”, он часто представляет что-то вроде “магического объекта, который Spring подложил”. Но на практике proxy — это просто обычный Java-объект, и в Java есть два базовых способа сделать “объект-обёртку”. Один способ опирается на интерфейсы (и встроен в JDK), другой — опирается на классы (и концептуально похож на наследование с переопределением методов). Spring умеет пользоваться обоими, и от того, какой именно proxy появился, зависят ваши ожидания: какой контракт вы можете использовать, и где вас ждёт “сюрприз” в виде неожиданного типа объекта.

Чтобы держать картинку в голове, полезно ещё раз нарисовать минимальный поток вызова:

%% Минимальная схема: вызов идёт через proxy к target (это и есть вся идея)
flowchart LR
    Caller["Вызывающий код"] --> P["Proxy (обёртка)"]
    P --> T["Target object (исходный объект)"]

Дальше вся лекция — про то, как именно выглядит блок Proxy в Java в двух разных вариантах.

2. JDK dynamic proxy: прокси по интерфейсам

JDK dynamic proxy — это, по сути, “официальная” встроенная в Java технология, которая позволяет в рантайме создать объект, реализующий один или несколько интерфейсов, и перехватывать вызовы методов этих интерфейсов через специальный обработчик. Это не Spring-специфика, это часть стандартной библиотеки (java.lang.reflect.Proxy). Важно сразу запомнить простое условие: JDK proxy работает только через интерфейсы. Если у вас есть хороший интерфейсный контракт — это очень удобный и прозрачный путь, который отлично ложится на DI-стиль “зависим от абстракций”.

Устройство JDK proxy “вручную”

JDK proxy создаётся через Proxy.newProxyInstance(...). Вы даёте ему:

1) ClassLoader (обычно от интерфейса),

2) список интерфейсов, которые proxy должен реализовывать,

3) обработчик вызовов (InvocationHandler) — функцию, которая получит Method и аргументы и решит, что делать.

В нашем ContextFlow у нас есть классический интерфейсный порт — NotificationSender. Возьмём целевой объект (target) и обернём его proxy-объектом, который напечатает “before/after” вокруг отправки.

import java.lang.reflect.Proxy;
import com.example.contextflow.domain.ports.NotificationSender;
import com.example.contextflow.infrastructure.notification.ConsoleNotificationSender;

// Target: реальная реализация, которая делает работу
NotificationSender target = new ConsoleNotificationSender();

// Proxy: объект-обёртка, который выглядит как NotificationSender,
// но на самом деле перехватывает вызовы через InvocationHandler
NotificationSender proxy = (NotificationSender) Proxy.newProxyInstance(
        NotificationSender.class.getClassLoader(),              // Берём ClassLoader интерфейса
        new Class
  []{NotificationSender.class},               // Говорим: прокси реализует этот интерфейс
        (p, method, args) -> {
            // Здесь мы можем добавить техническую обвязку ДО вызова
            System.out.println("[before] " + method.getName()); // [before] send

            // Делегируем вызов в настоящий target
            Object result = method.invoke(target, args);

            // И добавляем обвязку ПОСЛЕ вызова
            System.out.println("[after] " + method.getName());  // [after] send
            return result;                                      // Возвращаем результат вызывающему коду
        }
);

// Важно: вызывающий код работает с proxy как с обычным NotificationSender
proxy.send("Order created"); // SEND: Order created

Здесь важно понять не синтаксис (его можно “подсмотреть”), а модель. Вызов proxy.send("Order created") не идёт сразу в ConsoleNotificationSender. Он сначала попадает в обработчик, а уже обработчик решает “делегировать ли дальше”. Если делегирует — вызывает method.invoke(target, args).

Если вы сейчас ловите ощущение “это же почти декоратор!”, то вы на правильном пути. Да, по смыслу это очень близко к паттерну Decorator, только вместо ручного класса-обёртки вы используете механизм JDK, который создаёт прокси-класс за вас.

Интерфейсы в Spring: когда полезны

В ContextFlow многие ключевые точки вариативности оформлены именно интерфейсами: NotificationSender, AuditWriter, DiscountPolicy, OrderStore, OrderIdGenerator. Это не потому, что “в Spring так принято”, а потому что это реальные архитектурные границы между приложением и инфраструктурой/стратегиями. И это автоматически делает JDK proxy естественным кандидатом: он идеально работает, когда вызывающий код общается с зависимостью по интерфейсу.

Но важно не скатываться в “интерфейс на каждый чих”. Если вы сделаете интерфейс для каждого класса просто ради “а вдруг прокси”, вы получите проект, где навигация по коду будет выглядеть как экскурсия по лабиринту: “реализация” в одном месте, “интерфейс” в другом, а смысла — мало. В нашем проекте интерфейсы — это порты и стратегии, а не “украшение”.

Нюанс, который стоит заметить

JDK proxy реализует интерфейс(ы), но не обязан быть экземпляром конкретного класса реализации. То есть у вас сохраняется контракт интерфейса, но “точный класс” может оказаться неожиданным. Из-за этого почти сразу возникает практический вопрос: что именно покажут getClass() и instanceof, когда в переменной лежит proxy, а не знакомая реализация.

3. Class-based proxy: прокси по классу

Иногда хочется обернуть объект, но интерфейса у него нет. Или интерфейс есть, но исторически вы зависите от класса (бывает в легаси, бывает в учебных примерах, бывает “потому что так сложилось”). В таком случае появляется второй подход: proxy “по классу”. Концептуально он строится вокруг наследования: proxy — это подкласс, который переопределяет методы и добавляет поведение до/после вызова оригинальной логики. Spring в реальности генерирует такие прокси динамически (через байткод-инструменты), но нам сейчас важен принцип, а не кухня генерации классов.

Чтобы почувствовать это руками, сделаем максимально честную “ручную” иллюстрацию на классе без интерфейса. Пусть у нас есть сервис, который генерирует строку отчёта (упрощённо — “report-as-string”, как в наших ранних стадиях проекта).

public class ReportingService {

    public String generateDailyReport() {
        // Здесь находится "настоящая" логика сервиса (упрощённо)
        return "daily-report";
    }
}

Теперь “обёртка по классу” будет выглядеть как подкласс с переопределением:

public class TimedReportingService extends ReportingService {

    @Override
    public String generateDailyReport() {
        // Техническая обвязка: замер времени ДО вызова
        long started = System.nanoTime();

        // Важно: вызываем оригинальную реализацию через super
        String result = super.generateDailyReport();

        // Техническая обвязка: логируем ПОСЛЕ вызова
        System.out.println("Took: " + (System.nanoTime() - started)); // Took: 12345
        return result;
    }
}

И использование остаётся “по базовому типу”, как и положено полиморфизму:

// Ссылка базового типа, но реальный объект — подкласс (proxy по классу)
ReportingService service = new TimedReportingService();

System.out.println(service.generateDailyReport()); // daily-report

Смысл class-based proxy: вы по-прежнему держите ссылку типа ReportingService, но реальный объект — потомок, который добавляет поведение.

Когда нужен class-based proxy

Было бы очень удобно сказать: “всегда делайте интерфейсы, и будет вам счастье”. Но Spring живёт в мире, где:

- есть легаси-классы без интерфейсов (и их нельзя быстро переписать);

- есть сторонние классы, которые вы не контролируете;

- есть классы, где интерфейсный контракт искусственный и только мешает чтению.

Поэтому Spring умеет делать class-based proxy. Но за этот комфорт вы платите тем, что у наследования есть правила и ограничения Java. Именно они потом и определяют, какие методы реально можно оборачивать, а какие нет. Пока нам важно только зафиксировать: если proxy строится по классу, он концептуально “похож на подкласс”.

4. Выбор proxy в ContextFlow

В учебных объяснениях часто хочется выбрать “правильный ответ на все случаи”. Но в инженерии реальный “правильный ответ” почти всегда звучит так: “зависит”. В ContextFlow мы специально строили архитектуру так, чтобы ключевые зависимые точки были выражены интерфейсами. Это даёт нам гибкость и для DI, и для тестируемости, и (как бонус) для интерфейсных proxy. Поэтому практическая рекомендация здесь простая: там, где у вас порт/стратегия, держитесь за интерфейс; там, где у вас простой внутренний сервис без вариативности, не обязаны делать интерфейс “на всякий случай”.

Посмотрите на пример из проекта (он очень типичный для Spring и очень полезный для proxy-мышления). Сервис зависит не от ConsoleNotificationSender, а от контракта NotificationSender:

import org.springframework.stereotype.Service;
import com.example.contextflow.domain.ports.NotificationSender;

@Service
public class NotificationDispatchService {

    // Зависимость по интерфейсу: это ключевой момент для JDK proxy и DI в целом
    private final NotificationSender sender;

    public NotificationDispatchService(NotificationSender sender) {
        // Внедрение зависимости через конструктор:
        // контейнер может подложить как реальную реализацию, так и proxy-обёртку
        this.sender = sender;
    }
}

Почему это важно именно в контексте proxy? Потому что если контейнер (или инфраструктурная прослойка) вернёт вам JDK proxy, то он будет “виден” как NotificationSender. И всё прекрасно. А если бы вы захотели зависеть от конкретного ConsoleNotificationSender, вы бы жёстко привязались к классу — и любая интерфейсная обёртка могла бы стать проблемой.

При этом я не призываю делать “интерфейс для OrderPricingService только потому что он service”. Внутренние application-сервисы вполне могут оставаться классами, и если когда-нибудь вокруг них появится class-based proxy — это тоже нормально. Просто важно понимать, что вы получаете в обмен: JDK proxy лучше дружит с интерфейсными зависимостями, class-based proxy — с зависимостями по классу.

Сравнение JDK proxy и class-based proxy

Когда в голове одновременно живут термины “proxy”, “target”, “интерфейсы”, “наследование”, “контейнер”, мозг иногда пытается уйти в отпуск без предупреждения. Таблица ниже — это не “зубрилка”, а шпаргалка, к которой полезно возвращаться, когда вы читаете чужой код или странный stack trace. Сейчас нам важно зафиксировать ключевые различия именно на уровне поведения и требований, без байткода и внутренностей Spring.

Критерий JDK dynamic proxy Class-based proxy
На чём держится контракт На интерфейсах На классе (и его методах)
Что нужно, чтобы сделать proxy Нужен хотя бы один интерфейс Интерфейс не обязателен
Как перехватываются вызовы Через InvocationHandler (рефлексия Method) Концептуально через переопределение (в Spring — через сгенерированный подкласс)
Какой тип “удобнее” использовать в DI NotificationSender ReportingService
Что обычно хорошо проектировать в проекте так Порты и стратегии (*Sender, *Policy, *Store) Внутренние сервисы без интерфейсов, легаси-классы
Самая частая ошибка мышления “Можно сделать JDK proxy для класса без интерфейса” “Это просто объект того же класса, ничего не поменялось”

Этой таблицы достаточно, чтобы дальше не путать два семейства proxy и понимать, почему Spring “не мог выбрать один вариант и жить спокойно”. Он бы выбрал — но реальный мир не дал.

5. Мини-практика без Spring

Очень легко прочитать про proxy и сделать вид, что “ну да, понятно”. А потом впервые увидеть Proxy.newProxyInstance — и почувствовать, что Java внезапно стала похожа на фильм про шпионов. Поэтому полезно сделать маленькую практику в стиле “на коленке, но честно”: собрать две обёртки, одну — JDK dynamic proxy для интерфейса, вторую — class-based proxy через наследование. Пока нам важны сами формы proxy, без вмешательства контейнера.

Представим, что мы хотим удобно получать “обёрнутого уведомителя”. Можно сделать маленькую фабрику (в нашем проекте это могло бы жить где-нибудь в support.*, но сейчас важнее форма, а не точное место).

import java.lang.reflect.Proxy;
import com.example.contextflow.domain.ports.NotificationSender;

public class NotificationSenderProxies {

    public static NotificationSender withTiming(NotificationSender target) {
        return (NotificationSender) Proxy.newProxyInstance(
                NotificationSender.class.getClassLoader(),
                new Class
  []{NotificationSender.class},
                (p, method, args) -> {
                    long started = System.nanoTime();
                    Object result = method.invoke(target, args);
                    System.out.println("Took: " + (System.nanoTime() - started));
                    return result;
                }
        );
    }
}

И использование выглядит просто и “по интерфейсу”:

import com.example.contextflow.domain.ports.NotificationSender;
import com.example.contextflow.infrastructure.notification.ConsoleNotificationSender;

NotificationSender sender =
        NotificationSenderProxies.withTiming(new ConsoleNotificationSender());

sender.send("Order cancelled"); // SEND: Order cancelled

Теперь “похожая” практика для class-based proxy — это просто создание подкласса (как мы уже показывали):

ReportingService service = new TimedReportingService();

System.out.println(service.generateDailyReport()); // daily-report

В обоих случаях вы получили обёртку вокруг вызова метода. Но “точка опоры” разная: в одном случае вы стоите на интерфейсе, в другом — на классе. И это различие мы дальше будем постоянно встречать в Spring-коде.

6. Типичные ошибки при выборе proxy-модели

Ошибка №1: ожидать JDK proxy там, где нет интерфейса.
Это классическая ловушка: “ну я же могу обернуть объект… почему нельзя просто сделать proxy?”. Можно, но JDK dynamic proxy умеет быть proxy только для интерфейсов. Если интерфейса нет, Java не сможет создать объект “того же типа”, не прибегая к наследованию/генерации подкласса. Практический вывод простой: если вы хотите интерфейсную модель — дайте ей интерфейсный контракт, но делайте это там, где контракт реально нужен.

Ошибка №2: зависеть от конкретного класса там, где достаточно интерфейса.
В DI-коде это выглядит безобидно: “ну у меня же точно ConsoleNotificationSender, чего мне ваш NotificationSender?”. А потом внезапно появляется обёртка, и зависимость по конкретному классу превращается в проблему. В ContextFlow мы специально делали зависимости по портам-интерфейсам, чтобы не привязывать application-слой к деталям инфраструктуры и чтобы такие обёртки не ломали код.

Ошибка №3: пытаться “добавить поведение” и случайно перенести бизнес-логику в proxy.
Proxy — плохое место для бизнес-правил. Сегодня вы добавили логирование, завтра — “чуть-чуть проверим статус заказа”, послезавтра — “давайте тут же пересчитаем скидку”… и вот у вас два источника бизнес-логики: в target и в proxy. В результате отладка превращается в игру “угадай, где реально произошло действие”. Proxy должен делать техническую обвязку и оставлять смысл target’у.

Ошибка №4: писать proxy-код так, что он становится сложнее target’а.
Особенно это заметно в InvocationHandler: можно очень быстро превратить его в монстра с кучей if, обработкой десятка методов и ручным разруливанием всего подряд. Если обвязка становится сложной, это сигнал, что вы либо выбрали не тот уровень абстракции, либо вам нужен более структурированный механизм, чем “одна лямбда на все случаи жизни”. В учебных примерах мы держим proxy-код коротким и предсказуемым — и это хорошая привычка.

Ошибка №5: забывать, что class-based proxy опирается на переопределение методов.
На уровне идеи всё кажется простым: “сделали наследника — переопределили метод — завернули”. Но в Java есть ограничения: не всё можно переопределить, не всё можно “увидеть” через наследование, и есть вызовы, которые вообще обходят внешнюю обёртку. Это не “капризы Spring”, а правила языка. Поэтому у proxy-модели есть реальные границы, и важно держать их в голове заранее, а не удивляться им уже в дебаге.

1
Задача
Spring Core, 21 уровень, 1 лекция
Недоступна
JDK dynamic proxy для сервиса приветствий
JDK dynamic proxy для сервиса приветствий
1
Задача
Spring Core, 21 уровень, 1 лекция
Недоступна
Proxy по классу через наследование
Proxy по классу через наследование
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ