W3docs

Динамические прокси в Java

Создавайте прокси-реализации интерфейсов во время выполнения в Java с помощью java.lang.reflect.Proxy и InvocationHandler.

Динамический прокси — это объект, реализующий один или несколько интерфейсов, при этом каждый вызов метода перенаправляется — во время выполнения — через единственный обработчик, который вы пишете. JVM синтезирует прокси-класс на лету; вам не нужно писать реализацию. Это самый мощный уголок java.lang.reflect, и именно так работают AOP, прозрачное логирование, ленивая загрузка, RPC-заглушки и библиотеки мокирования. В этой главе показано, как Proxy.newProxyInstance и InvocationHandler работают вместе и что они могут и не могут делать.

Два компонента: Proxy и InvocationHandler

Динамический прокси требует трёх параметров:

  1. Загрузчик классов (где определить синтезируемый класс).
  2. Массив интерфейсов, которые прокси будет реализовывать.
  3. InvocationHandler — единственный метод, который принимает каждый вызов.
InvocationHandler handler = (proxy, method, args) -> {
  // called for EVERY method invoked on the proxy
  return ...;   // becomes the method's return value
};

MyService svc = (MyService) Proxy.newProxyInstance(
    MyService.class.getClassLoader(),
    new Class<?>[]{ MyService.class },
    handler);

svc — это теперь реальный объект, реализующий MyService. Вызов svc.doThing(x) не выполняет никакого тела doThing — его не существует — он вызывает handler.invoke(proxy, <Method doThing>, [x]). Обработчик решает, что делать и что возвращать.

Сигнатура invoke

Object invoke(Object proxy, Method method, Object[] args) throws Throwable
  • proxy — сам экземпляр прокси (используется редко; будьте осторожны при вызове методов на нём изнутри invoke — это повторно входит в обработчик и может привести к бесконечной рекурсии).
  • method — вызванный Method; доступны method.getName(), method.getReturnType(), его аннотации и так далее.
  • args — аргументы в виде Object[] (null, если метод не принимает аргументов); примитивы упакованы в объекты.
  • Возвращаемое значение — то, что должен получить вызывающий код; должно быть совместимо по присваиванию с method.getReturnType(), иначе возникнет ClassCastException. Для метода void возвращайте null.

Распространённый паттерн — перенаправление к реальному «целевому» объекту: method.invoke(target, args) с логированием, замером времени, транзакциями или повтором. Этот вызов Method.invoke — то же рефлексивное перенаправление, которое рассматривалось в Java Reflection: методы; здесь он полностью управляется объектом Method, который JVM передаёт вашему обработчику. Эта схема перенаправления — декоратор через прокси, и именно на ней основан Spring AOP.

Только интерфейсы

Главное ограничение: java.lang.reflect.Proxy проксирует интерфейсы, а не классы. С помощью этого API нельзя создать динамический прокси для конкретного класса. Если нужно проксировать класс, вы обращаетесь к библиотеке байткода (CGLIB, ByteBuddy), которая генерирует подкласс — именно поэтому фреймворки поставляются с этими библиотеками. Для проектирования на основе интерфейсов встроенный Proxy достаточен и не требует зависимостей.

Синтезируемый прокси-класс:

  • Расширяет java.lang.reflect.Proxy и реализует ваши интерфейсы.
  • Имеет сгенерированное имя вида $Proxy0.
  • Также направляет equals, hashCode и toString (методы Object) через invoke — поэтому ваш обработчик должен быть готов их обрабатывать или разумно делегировать.

Рабочий пример: прокси с логированием и замером времени

Программа определяет интерфейс Repository и реальную реализацию, затем оборачивает реализацию в динамический прокси, обработчик которого логирует каждый вызов, замеряет время, перенаправляет к реальному объекту и логирует результат — добавляя сквозную функциональность без изменения реализации.

java— editable, runs on the server

Что следует вынести из запуска:

  • repo использовался в точности как Repositoryrepo.save(...), repo.count(), repo.find(...) — всё компилировалось и выполнялось — при этом в исходном коде нет класса с названием «logging repository». JVM сгенерировала класс $Proxy0, реализующий интерфейс, и каждый вызов попадал в LoggingHandler.invoke. Прокси является настоящим Repository (instanceof вернул true).
  • Каждый бизнес-метод автоматически получил логирование входа/выхода и замер времени без каких-либо изменений в InMemoryRepository. Это разделение — реализация ничего не знает, сквозная функциональность живёт в обработчике — и есть суть AOP. Именно так Spring реализует @Transactional, @Cacheable и подобные механизмы для бинов на основе интерфейсов.
  • Обработчик перенаправлял каждый вызов через method.invoke(target, args), что означает: при ошибке find(99) возвращается InvocationTargetException. Обработчик извлёк причину через getCause() и повторно выбросил реальный NoSuchElementException, чтобы вызывающий код поймал естественное исключение, а не обёртку из рефлексии. Прокси, забывший распаковать исключение, утечёт InvocationTargetException до вызывающего кода.
  • Методы Object тоже направляются через invoke, поэтому обработчик выделил случай method.getDeclaringClass() == Object.class и передал их напрямую. Без этой проверки toString/equals/hashCode тоже логировались бы (что создаёт шум) или, если бы вы строили строки из прокси внутри invoke, могли бы вызвать рекурсию. Явная обработка методов Object — стандартная часть написания обработчика прокси.
  • Proxy.isProxyClass(repo.getClass()) подтвердил, что класс синтезирован JVM, а имя $Proxy0 показывает, что он сгенерирован, а не написан вручную. Поскольку API принимает Class<?>[] интерфейсов, один прокси может реализовывать несколько сразу — именно так один мок или заглушка могут одновременно удовлетворять нескольким контрактам.

Когда что использовать

  • Интерфейс, зависимости не нужныjava.lang.reflect.Proxy. Встроен, прост, только для интерфейсов.
  • Нужно проксировать конкретный класс → ByteBuddy или CGLIB (на основе подклассов). Необходимы, потому что Proxy не может этого сделать.
  • Нужно только заглушить интерфейсы в тестах → библиотека мокирования (Mockito), построенная на этих механизмах — не изобретайте велосипед.

Динамические прокси завершают часть, посвящённую рефлексии: от инспектирования Class-объектов, до чтения и записи полей, вызова методов, создания экземпляров через конструкторы, чтения аннотаций и, наконец, синтеза целых реализаций во время выполнения. Вместе они образуют инструментарий, позволяющий фреймворкам обобщённо работать с типами, против которых они не были скомпилированы — используемые экономно и за чистыми абстракциями, именно они делают возможной экосистему Java: контейнеры, маперы и запускатели.

Практика

Практика
Вы хотите обернуть сервис, определённый интерфейсом 'PaymentGateway', чтобы каждый вызов метода логировался без изменения реальной реализации. Вы вызываете 'Proxy.newProxyInstance(...)' с 'new Class<?>[]{ PaymentGateway.class }' и обработчиком. Внутри 'invoke' обработчика, что является стандартным способом получить фактический результат метода?
Вы хотите обернуть сервис, определённый интерфейсом 'PaymentGateway', чтобы каждый вызов метода логировался без изменения реальной реализации. Вы вызываете 'Proxy.newProxyInstance(...)' с 'new Class<?>[]{ PaymentGateway.class }' и обработчиком. Внутри 'invoke' обработчика, что является стандартным способом получить фактический результат метода?
Was this page helpful?