Динамический прокси-механизм технологии Java

Java

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

Отражение технологии Java
«Технология Java как механизм загрузки»

статический прокси

Обычный шаблон прокси состоит из следующих трех частей:
функциональный интерфейс

interface IFunction {
	void doAThing();
}

поставщик функций

class FunctionProvider implement IFunction {
	public void doAThing {
		System.out.print("do A");
	}
}

функциональный агент

class Proxy implement IFunction {
	private FunctionProvider provider;
	Proxy(FunctionProvider provider) {
		this.provider = provider;
	}
	public void doAThing {
		provider.doAThing();
	}
}

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

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

Однако у этого метода есть и недостатки: например, если есть несколько интерфейсов, которые необходимо проксировать, то для каждого провайдера функции необходимо создать соответствующий класс прокси, который будет становиться все больше и больше. Кроме того, так называемый «статический» прокси означает, что делегированный класс, который будет проксироваться, должен быть известен заранее.

Это иллюстрируется следующим примером:

Функция статистики отнимает много времени – реализация статического прокси

Теперь я надеюсь использовать прокси-класс для выполнения временных статистических данных о методах, которые меня интересуют. Использование статического прокси, достигается следующие реализации:

interface IAFunc {
	void doA();
}
interface IBFunc {
	void doB();
}
class TimeConsumeProxy implement IAFunc, IBFunc {
	private AFunc a;
	private BFunc b;
	public(AFunc a, BFunc b) {
		this.a = a;
		this.b = b;
	}
	void doA() {
		long start = System.currentMillions();
		a.doA();
		
		System.out.println("耗时:" + (System.currentMillions() - start));
	}
	void doB() {
		long start = System.currentMillions();
		b.doB();
		
		System.out.println("耗时:" + (System.currentMillions() - start));
	}
}

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

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

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

Основная идея динамического прокси заключается в динамическом создании прокси-объекта для любого входящего объекта через класс Java Proxy.Этот прокси-объект по умолчанию реализует все интерфейсы исходного объекта.

Более прямолинейно проиллюстрировать трудоемкий пример статистических функций.

Статистическая функция Потребляющая трудоемкость - Динамическая реализация прокси

interface IAFunc {
	void doA();
}
interface IBFunc {
	void doB();
}
class A implement IAFunc { ... }
class B implement IBFunc { ... }
class TimeConsumeProxy implements InvocationHandler {
	private Object realObject;
	public Object bind(Object realObject) {
		this.realObject = realObject;
		Object proxyObject = Proxy.newInstance(
			realObject.getClass().getClassLoader(),
			realObject.getClass().getInterfaces(),
			this
		);
		return proxyObject;
	}
	@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentMillions();
        Object result = method.invoke(target, args);
        System.out.println("耗时:" + (System.currentMillions() - start));
        return result;
    }
}

При его использовании конкретно:

public static void main(String[] args) {
	A a = new A();
	IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
	aProxy.doA();
	B b = new B();
	IBFunc bProxy = (IBFunc) new TimeConsumeProxy().bind(b);
	bProxy.doB();	
}

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

Вернемся к самому динамическому прокси, самое главное в приведенном выше коде:

Object proxyObject = Proxy.newInstance(
			realObject.getClass().getClassLoader(),
			realObject.getClass().getInterfaces(),
			this
		);

С помощью инструмента Proxy реальный класс делегирования преобразуется в прокси-класс.В начале упоминаются три элемента прокси-модели: функциональный интерфейс, функциональный поставщик и функциональный прокси; соответствующие здесь:realObject.getClass().getInterfaces(),realObject,TimeConsumeProxy.

На самом деле, динамический прокси не сложен.С помощью инструмента Proxy автоматически создается прокси-объект для интерфейса делегированного класса.Последующие вызовы функций инициируются через этот прокси-объект и в конечном итоге будут выполняться дляInvocationHandler#invokeметод, в этом методе, в дополнение к вызову метода, соответствующего реальному классу делегата, вы также можете выполнять некоторую другую пользовательскую логику, например приведенную выше статистику времени выполнения и т. д.

Изучите механизм реализации динамического прокси

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

Набралось несколько вопросов:

  1. Прокси-объект, сгенерированный вышеObject proxyObjectЧто именно? почему его можно превратить вIAFunc, также можно позвонитьdoA()метод?
  2. этоproxyObjectКак он генерируется? Это класс?

Позвольте мне сначала дать ответ, а затем пошагойте на шаг, чтобы исследовать, как пришел этот ответ.

Вопрос 1: Что такое proxyObject -> динамически генерируемый файл $Proxy0.class

вызовProxy.newInstance, Java закончится для класса делегатаAСоздайте настоящий файл класса:$Proxy0.class,а такжеproxyObjectявляется экземпляром этого класса.

Угадай это$Proxy0.classКак выглядит класс и какие методы он содержит? Посмотрите на код только что:

IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
aProxy.doA();

По рассуждениям понятно, что это$Proxy0.classДостигнутоIAFuncинтерфейс, а также внутренне реализуетdoA()метод, и дело в том, что этоdoA()Метод будет выполняться во время выполненияTimeConsumeProxy#invoke()метод.

Вот в чем дело! Давайте посмотрим на это$Proxy0.classфайл, поместите его в IDE для декомпиляции, вы можете увидеть следующее содержимое, чтобы проверить предположение прямо сейчас:

final class $Proxy0 extends Proxy implements IAFunc {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
    public final boolean equals(Object var1) throws  {
        // 省略
    }
    public final void doA() throws  {
        try {
        	// 划重点
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    public final String toString() throws  {
        // 省略
    }
    public final int hashCode() throws  {
        // 省略
    }
    static {
        try {
        	// 划重点
            m3 = Class.forName("proxy.IAFunc").getMethod("doA", new Class[0]);
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

Правильно, с догадками сейчас все в порядке! ДостигнутоIAFuncинтерфейс иdoA()метод, однакоdoA()Что, черт возьми, там?

super.h.invoke(this, m3, (Object[])null);

Оглядываться,TimeConsumeProxyвнутриinvokeметод, какова его сигнатура функции?

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

Вот так,doA()Все, что он делает, это звонитTimeConsumeProxy#invoke()метод.

То есть поток выполнения следующего кода выглядит следующим образом:

IAFunc aProxy = (IAFunc) new TimeConsumeProxy().bind(a);
aProxy.doA();

  1. На основе входящего класса делегатаA, генерирует$Proxy0.classдокумент;
  2. Создавать$Proxy0.classобъект, превращенный вIAFuncинтерфейс;
  3. передачаaProxy.doA(), автоматически вызывать TimeConsumeProxy внутреннийinvokeметод.

Вопрос 2: Как шаг за шагом генерируется proxyObject -> процесс генерации файла $Proxy0.class

Только что взглянув на результаты с конца, теперь вернемся к началу кода:

Object proxyObject = Proxy.newInstance(
			realObject.getClass().getClassLoader(),
			realObject.getClass().getInterfaces(),
			this
		);

Готов приступить к чтению исходного кода. Я возьму важный код и добавлю комментарии.

Первый взглядProxy.newInstance():

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    // 复制要代理的接口                                  	
	final Class<?>[] intfs = interfaces.clone();
	// 重点:生成 $Proxy0.class 文件并通过 ClassLoader 加载进来
	Class<?> cl = getProxyClass0(loader, intfs);
	// 对 $Proxy0.class 生成一个实例,就是 `proxyObject`
	final Constructor<?> cons = cl.getConstructor(constructorParams);
	return cons.newInstance(new Object[]{h});
}

увидеть сноваgetProxyClass0Конкретная реализация:ProxyClassFactoryЗаводской класс:

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
	// 参数为 ClassLoader 和要代理的接口
	Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
	// 1. 验证 ClassLoader 和接口有效性
	for (Class<?> intf : interfaces) {
		// 验证 classLoader 正确性
		Class<?> interfaceClass = Class.forName(intf.getName(), false, loader);
		if (interfaceClass != intf) {
            throw new IllegalArgumentException(
                intf + " is not visible from class loader");
        }
		// 验证传入的接口 class 有效
		if (!interfaceClass.isInterface()) { ... } 
		// 验证接口是否重复
		if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) { ... }
	}
	// 2. 创建包名及类名 $Proxy0.class
	proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
	long num = nextUniqueNumber.getAndIncrement();
    String proxyName = proxyPkg + proxyClassNamePrefix + num;
    // 3. 创建 class 字节码内容
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
    // 4. 基于字节码和类名,生成 Class<?> 对象
    return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
}

Посмотрите на третий шаг для создания содержимого классаProxyGenerator.generateProxyClass:

// 添加 hashCode equals toString 方法
addProxyMethod(hashCodeMethod, Object.class);
addProxyMethod(equalsMethod, Object.class);
addProxyMethod(toStringMethod, Object.class);
// 添加委托类的接口实现
for (int i = 0; i < interfaces.length; i++) {
    Method[] methods = interfaces[i].getMethods();
    for (int j = 0; j < methods.length; j++) {
         addProxyMethod(methods[j], interfaces[i]);
    }
}
// 添加构造函数
methods.add(this.generateConstructor());

Здесь конструируется содержимое класса: добавляем необходимые функции, реализуем интерфейсы, конструкторы и т.д., далее нужно написать то, что вы видели на предыдущем шаге$Proxy0.class.

ByteArrayOutputStream bout = new ByteArrayOutputStream();
DataOutputStream dout = new DataOutputStream(bout);
dout.writeInt(0xCAFEBABE);
...
dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
...
return bout.toByteArray();

Здесь создается первый видимый шаг$Proxy0.classФайл завершен, замкнутый цикл завершен, и объяснение завершено!

Сводка динамических прокси

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

За динамическим прокси он фактически генерируется динамически для класса делегата.$Proxy0.classПрокси-класс прокси-класса реализует интерфейс класса делегата и перенаправляет вызов интерфейса вInvocationHandler#invokeНаконец, вызывается соответствующий метод реального класса делегата.

Механизм динамического прокси изолирует класс делегата от класса прокси и улучшает расширяемость.

Динамические прокси Java и декораторы Python

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


Благодарю.

wingjay