Динамический прокси JDK: не только научиться пользоваться, но и освоить его принципы

Java
Динамический прокси JDK: не только научиться пользоваться, но и освоить его принципы

Поиск в WeChat: Code Farmer StayUp

Адрес домашней страницы:gozhuyinglong.github.io

Совместное использование исходного кода:GitHub.com/go Основной бизнес…

Динамический прокси JDK означает, что экземпляр класса прокси динамически генерируется JVM в соответствии с механизмом отражения во время работы программы. То есть прокси-класс не определяется пользователем, а генерируется JVM.

Поскольку его принцип реализуется через механизм отражения Java, перед обучением необходимо иметь определенное представление о механизме отражения. Портал:Механизм отражения Java: следуйте коду, чтобы изучить отражение

Ниже приводится содержание этой статьи:

1. Основной класс динамического прокси JDK

Динамический прокси JDK имеет два основных класса, которые находятся в пакете отражения Java (java.lang.reflect), соответственноInvocationHandlerинтерфейс иProxyДобрый.

1.1 Интерфейс InvocationHandler

Необходимо реализовать обработчик вызова экземпляра прокси.InvocationHandlerИнтерфейс, и у каждого экземпляра агента есть связанный с ним вызывающий процессор. Этот вызов метода будет закодирован и назначен вызывающему процессору, когда метод вызывается на экземпляре прокси.invokeметод.

То есть каждый экземпляр прокси, который мы создаем, должен иметь связанныйInvocationHandler, а при вызове метода экземпляра прокси он будет переадресован наInvocationHandlerизinvokeметод.

public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;

ДолженinvokeЦель метода — обработать вызов метода на экземпляре прокси и вернуть результат.

Он имеет три параметра, а именно:

  • proxy: это экземпляр прокси, который вызывает метод.
  • method: соответствует методу интерфейса, вызываемому в экземпляре прокси.Methodпример.
  • args:ОдинObjectМассив значений параметров, передаваемых при вызове метода экземпляра прокси. Если метод интерфейса не имеет параметров, значение равно null.

Его возвращаемое значение: возвращаемое значение вызова метода на экземпляре прокси.

1.2 Класс прокси

ProxyКласс предоставляет статические методы для создания динамических прокси-классов и их экземпляров, которые также являются надклассом динамических прокси-классов.

Прокси-класс имеет следующие свойства:

  • Имя прокси-класса начинается с «$Proxy», за которым следует числовой порядковый номер.
  • Прокси-класс наследуетProxyДобрый.
  • Класс прокси реализует интерфейс, указанный во время создания (динамические прокси JDK ориентированы на интерфейс).
  • Каждый прокси-класс имеет общедоступный конструктор, который принимает один параметр — интерфейс.InvocationHandlerРеализация обработчика вызова, используемого для установки экземпляра прокси.

ProxyДля получения прокси-объекта предусмотрено два статических метода.

1.2.1 getProxyClass

используется для получения прокси-классаClassобъект, а затем создайте экземпляр прокси, вызвав конструктор.

public static Class<?> getProxyClass(ClassLoader loader,
                                         Class<?>... interfaces)
        throws IllegalArgumentException

Метод имеет два параметра:

  • loader: для загрузчика классов.
  • intefaces: для интерфейсаClassмассив объектов.

Возвращаемое значение — динамический прокси-класс.Classобъект.

1.2.2 newProxyInstance

Используется для создания экземпляра прокси.

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException

Метод имеет три параметра:

  • loader: для загрузчика классов.
  • interfaces: для интерфейсаClassмассив объектов.
  • h: указанный обработчик вызова.

Возвращаемое значение является экземпляром прокси-класса для указанного интерфейса.

1.3 Резюме

ProxyКласс в основном используется для получения динамических прокси-объектов,InvocationHandlerИнтерфейсы в основном используются для ограничения и улучшения вызовов методов.

2. Пример кода для получения экземпляра прокси

Два статических метода для получения экземпляра прокси были представлены в предыдущей главе, а теперь их реализация демонстрируется на примере кода.

2.1 Создайте целевой интерфейс и класс его реализации

Динамический прокси JDK основан на интерфейсе, мы создаем интерфейс и класс его реализации.

Фу интерфейс:

public interface Foo {

    String ping(String name);

}

Класс реализации RealFoo интерфейса Foo:

public class RealFoo implements Foo {

    @Override
    public String ping(String name) {
        System.out.println("ping");
        return "pong";
    }

}

2.2 Создание обработчика вызовов

СоздаватьInvocationHandlerКласс реализации интерфейса MyInvocationHandler. Параметр конструктора этого класса является целевым объектом для проксирования.

invokeТри параметра метода были введены выше путем вызоваmethodизinvokeметод для завершения вызова метода.

Неважно, если вы какое-то время этого не понимаете, глава анализа исходного кода проанализирует это позже.

public class MyInvocationHandler implements InvocationHandler {

    // 目标对象
    private final Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy - " + proxy.getClass());
        System.out.println("method - " + method);
        System.out.println("args - " + Arrays.toString(args));
        return method.invoke(target, args);
    }
}

2.3 Метод 1: Получите экземпляр прокси с помощью метода getProxyClass

Конкретные этапы реализации следующие:

  1. Получить объект класса прокси-класса на основе загрузчика классов и массива интерфейса
  2. Создать экземпляр через конструктор объекта Class (экземпляр прокси-класса)
  3. Принуждает экземпляр прокси к целевому интерфейсу Foo (поскольку класс прокси реализует целевой интерфейс, его можно принудить).
  4. Наконец, используйте прокси для вызова метода.
@Test
public void test1() throws Exception {
    Foo foo = new RealFoo();
    // 根据类加载器和接口数组获取代理类的Class对象
    Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);

    // 通过Class对象的构造器创建一个实例(代理类的实例)
    Foo fooProxy = (Foo) proxyClass.getConstructor(InvocationHandler.class)
        .newInstance(new MyInvocationHandler(foo));

    // 调用 ping 方法,并输出返回值
    String value = fooProxy.ping("杨过");
    System.out.println(value);

}

Выходной результат:

proxy - class com.sun.proxy.$Proxy4
method - public abstract java.lang.String io.github.gozhuyinglong.proxy.Foo.ping(java.lang.String)
args - [杨过]
ping
pong

Из вывода видно, что:

  • Имя прокси-класса$Proxyначало.
  • Экземпляр метода — это метод, вызываемый прокси-классом.
  • Параметр — это параметр, который передается, когда прокси-класс вызывает метод.

2.4 Метод 2: Получите экземпляр прокси с помощью метода newProxyInstance

Этот метод является самым простым и рекомендуемым, и прокси-объект можно получить напрямую с помощью этого метода.

Примечание. На самом деле фоновая реализация этого метода аналогична описанному выше процессу использования метода getProxyClass.

@Test
public void test2() {
    Foo foo = new RealFoo();
    // 通过类加载器、接口数组和调用处理器,创建代理类的实例
    Foo fooProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
                                                new Class[]{Foo.class},
                                                new MyInvocationHandler(foo));
    String value = fooProxy.ping("小龙女");
    System.out.println(value);
}

2.5 Упрощенная реализация с помощью лямбда-выражений

фактическиInvocationHanderИнтерфейсу не нужно создавать класс реализации, и он может использовать выражения Lambad для упрощенной реализации следующим образом:

@Test
public void test3() {
    Foo foo = new RealFoo();

    Foo fooProxy = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
                                                new Class[]{Foo.class},
                                                (proxy, method, args) -> method.invoke(foo, args));
    String value = fooProxy.ping("雕兄");
    System.out.println(value);
}

3. Анализ исходного кода

3.1 Как выглядит прокси-класс $Proxy

Как именно выглядит прокси-класс, который JVM автоматически генерирует для нас? Давайте сначала сгенерируем его, а потом посмотрим на структуру внутри.

3.1.1 Создайте файл .class для $Proxy

JVM не создает файл .class по умолчанию, вам нужно добавить параметр запуска:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

Нажмите [Edit Configurations...] в IDEA, чтобы открыть окно конфигурации Run/Debug Configurations.

Добавьте вышеуказанные параметры запуска в [Параметры VM] и нажмите [OK].

Запустите код еще раз, вы найдете файл .class в каталоге [com.sun.proxy] в проекте, вот "$Proxy4.class"

3.1.2 Почему файл байт-кода $Proxy может быть создан путем добавления этого параметра запуска

существуетProxyкласс имеетProxyClassFactoryСтатический внутренний класс, основная функция этого класса — создание статического прокси.

Там есть кусок кодаProxyGenerator.generateProxyClassФайл .class, используемый для создания прокси-классов.

где переменнаяsaveGeneratedFilesНа значение этого параметра запуска ссылаются. Настройте этот параметр запуска какtrueБудет создан файл .class.

3.1.3 Как выглядит этот прокси-класс $Proxy?

Таинственная завеса вот-вот откроется, и здесь можно найти ответы на многие неразгаданные тайны!

открыть это$Proxyфайл, который я сгенерировал здесь$Proxy4, следующее содержание:

// 该类为final类,其继承了Proxy类,并实现了被代理接口Foo
public final class $Proxy4 extends Proxy implements Foo {

    // 这4个Method实例,代表了本类实现的4个方法
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    // 静态代码块根据反射获取这4个方法的Method实例
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("io.github.gozhuyinglong.proxy.Foo").getMethod("ping");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }

    // 一个公开的构造函数,参数为指定的 InvocationHandler 
    public $Proxy4(InvocationHandler var1) throws  {
        super(var1);
    }
    
    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    // Foo接口的实现方法,最终调用了 InvocationHandler 中的 invoke 方法
    public final String ping(String var1) throws  {
        try {
            return (String)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
}

Из этого документа видно, что:

  • Прокси-класс наследуетProxyкласс, основная цель которого - пройтиInvocationHandler.
  • Прокси-класс реализует прокси-интерфейс Foo, поэтому прокси-класс можно напрямую привести к интерфейсу.
  • имеет публичный конструктор с указанными параметрамиInvocationHandlerи передать параметр родительскому классуProxyсередина.
  • Каждый реализованный метод будет вызыватьсяInvocationHandlerсерединаinvokeметод и передать три параметра самого прокси-класса, экземпляр метода и входные параметры. Вот почему, когда вызывается метод в прокси-классе, он всегда отправляется вInvocationHandlerсерединаinvokeпричина метода.

3.2 Как создается прокси-класс

мы начинаем сProxyКласс предоставляет нам два статических метода для запускаgetProxyClassа такжеnewProxyInstance. Как описано выше, эти два метода используются для создания прокси-классов и их экземпляров.Давайте посмотрим на исходный код.

3.2.1 Методы getProxyClass и newProxyInstance

getProxyClass方法

newProxyInstance方法

Как видно из приведенного выше исходного кода, эти два метода в итоге будут вызыватьсяgetProxyClass0метод создания прокси-классаClassобъект. ТолькоnewProxyInstanceметод создает экземпляр прокси для нас, иgetProxyClassМетод требует, чтобы мы сами создали экземпляр прокси.

3.2.2 Метод getProxyClass0

Давайте посмотрим на эту объединенную запись:getProxyClass0 getProxyClass0方法

Как видно из исходного кода и комментариев:

  • Максимальное количество прокси-интерфейсов не может превышать 65535.
  • Класс прокси сначала будет получен из кеша, потом не пройдетProxyClassFactoryСоздайте прокси-класс. (Прокси-классы кэшируются на определенный период времени.)

3.2.3 Класс WeakCache

Вот краткое введениеWeakCache<K, P, V> class, который в основном кэшируется для прокси-классов. При получении прокси-класса он сначала будет получен из кеша, если нет, то будет вызванProxyClassFactoryКласс создается и кэшируется после его создания.WeakCache类的get方法

3.2.4 Класс ProxyClassFactory

ProxyClassFactoryдаProxyСтатический внутренний класс класса, который используется для создания прокси-классов. Следующий рисунок является частью исходного кода:

ProxyClassFactory类

  • Здесь определяется имя прокси-класса и его префикс$Proxy, с добавлением числа.
  • перечислитьProxyGenerator.generateProxyClassдля создания указанного прокси-класса.
  • defineClass0метод представляет собойnativeметод, отвечающий за реализацию загрузки байт-кода, и возвращает соответствующийClassобъект.

3.3 Схема

Для удобства записи процесс генерации прокси-класса организован в виде диаграммы.

Совместное использование исходного кода

Пожалуйста, посетите мой Github для получения полного кода.Если он вам полезен, поставьте ⭐, спасибо~~🌹🌹🌹

GitHub.com/go Основной бизнес…

Рекомендуемое чтение

Об авторе

проект содержание
Нет публики Код Farmer StayUp (ID: AcmenStayUp)
Домой gozhuyinglong.github.io
CSDN blog.CSDN.net/go Основной бизнес...
раскопки Талант /user/123990…
Github GitHub.com/go Основной бизнес…
Gitee git ee.com/go - основной бизнес...