Механизм отражения и механизм сборки мусора в 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
метод, в этом методе, в дополнение к вызову метода, соответствующего реальному классу делегата, вы также можете выполнять некоторую другую пользовательскую логику, например приведенную выше статистику времени выполнения и т. д.
Изучите механизм реализации динамического прокси
Что ж, выше мы объяснили основы использования динамического прокси и зачем использовать динамический прокси, Многие статьи почти здесь, но мы готовы продолжить изучение для заинтересованных читателей.
Набралось несколько вопросов:
- Прокси-объект, сгенерированный выше
Object proxyObject
Что именно? почему его можно превратить вIAFunc
, также можно позвонитьdoA()
метод? - это
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();
- На основе входящего класса делегата
A
, генерирует$Proxy0.class
документ; - Создавать
$Proxy0.class
объект, превращенный вIAFunc
интерфейс; - передача
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