Динамические прокси в Java
Создавайте прокси-реализации интерфейсов во время выполнения в Java с помощью java.lang.reflect.Proxy и InvocationHandler.
Динамический прокси — это объект, реализующий один или несколько интерфейсов, при этом каждый вызов метода перенаправляется — во время выполнения — через единственный обработчик, который вы пишете. JVM синтезирует прокси-класс на лету; вам не нужно писать реализацию. Это самый мощный уголок java.lang.reflect, и именно так работают AOP, прозрачное логирование, ленивая загрузка, RPC-заглушки и библиотеки мокирования. В этой главе показано, как Proxy.newProxyInstance и InvocationHandler работают вместе и что они могут и не могут делать.
Два компонента: Proxy и InvocationHandler
Динамический прокси требует трёх параметров:
- Загрузчик классов (где определить синтезируемый класс).
- Массив интерфейсов, которые прокси будет реализовывать.
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 Throwableproxy— сам экземпляр прокси (используется редко; будьте осторожны при вызове методов на нём изнутри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 и реальную реализацию, затем оборачивает реализацию в динамический прокси, обработчик которого логирует каждый вызов, замеряет время, перенаправляет к реальному объекту и логирует результат — добавляя сквозную функциональность без изменения реализации.
Что следует вынести из запуска:
repoиспользовался в точности какRepository—repo.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: контейнеры, маперы и запускатели.