:notebook: Эта статья была заархивирована в: "blog"
:keyboard: Пример кода в этой статье был заархивирован в: "javacore"
Введение
что такое отражение
Рефлексия — это одна из возможностей языка разработки программ Java, позволяющая работающей программе Java получать собственную информацию и манипулировать внутренними свойствами класса или объекта.
Через механизм отражения можно получить доступ к свойствам, методам, конструкторам и т. д. объектов Java во время выполнения.
Сценарии применения рефлексии
Основными сценариями применения рефлексии являются:
- разработать общую структуру- Наиболее важным применением рефлексии является разработка различных общих рамок. Многие фреймворки (такие как Spring) настраиваются (например, настройка JavaBeans, фильтров и т. д. через XML-файлы).Для обеспечения универсальности фреймворка может потребоваться загрузка разных объектов или классов в соответствии с конфигурационным файлом и вызов различные методы.В это время вы должны использовать отражение - динамически загружать объекты, которые должны быть загружены во время выполнения.
- Динамический прокси- В аспектном программировании (АОП) необходимо перехватывать определенный метод, обычно выбирается метод динамического прокси. В настоящее время для достижения этого необходима технология отражения.
- аннотация- Сама аннотация используется только для маркировки, необходимо использовать механизм отражения для вызова интерпретатора аннотации и выполнения поведения в соответствии с аннотацией. Без отражения аннотации не более полезны, чем аннотации.
- Возможности расширения- Приложения могут использовать внешние пользовательские классы, создавая экземпляры объектов расширения с полными именами.
Недостатки отражения
- накладные расходы на производительность- Невозможно выполнить некоторые оптимизации виртуальной машины Java, поскольку отражение включает динамически разрешенные типы. Следовательно, отражающие операции работают хуже, чем неотражающие, и их следует избегать в часто вызываемых разделах кода в приложениях, чувствительных к производительности.
- разорвать инкапсуляцию- Проверки разрешений можно игнорировать при вызове методов путем отражения, что может нарушить инкапсуляцию и вызвать проблемы с безопасностью.
- Внутреннее воздействие- Поскольку отражение позволяет коду выполнять операции, недопустимые в нерефлексивном коде, такие как доступ к закрытым полям и методам, использование отражения может привести к непреднамеренным побочным эффектам, которые могут вызвать сбой в работе кода и потенциально нарушить переносимость. Отражающий код разрушает абстракции и, следовательно, может изменить поведение по мере обновления платформ.
механизм отражения
процесс загрузки класса
Полный процесс загрузки класса выглядит следующим образом:
(1) Во время компиляции компилятор Java компилирует.java
После создания файла на диске.class
документ..class
Файл — это двоичный файл, содержимое которого представляет собой машинный код, понятный только JVM.
(2) Загрузчик классов в JVM читает файл байт-кода, извлекает двоичные данные, загружает их в память и анализирует информацию в файле .class. Загрузчик класса получает двоичный поток байтов этого класса в соответствии с полным именем класса; затем он преобразует статическую структуру хранения, представленную потоком байтов, в структуру данных времени выполнения области метода;java.lang.Class
объект.
(3) После загрузки JVM начинает фазу подключения (включая проверку, подготовку и инициализацию). После этой серии операций переменные класса будут инициализированы.
Объект класса
Чтобы использовать отражение, вам сначала нужно получить объект класса, соответствующий классу, которым нужно управлять.В Java независимо от того, сколько объектов класса создано, эти объекты будут соответствовать одному и тому же объекту класса. Этот объект класса генерируется JVM, с помощью которого он может изучить структуру всего класса.. так,java.lang.Class
Может считаться точкой входа для всех API отражения.
Суть отражения заключается в отображении различных компонентов классов Java в объекты Java во время выполнения.
Например, предположим, что определен следующий код:
User user = new User();
Инструкции по шагам:
- Когда JVM загружает метод, она встречает
new User()
, JVM будетUser
полное имя для загрузкиUser.class
. - JVM отправится на локальный диск, чтобы найти
User.class
файл и загрузить его в память JVM. - JVM автоматически создает соответствующий класс для этого класса, вызывая загрузчик классов.
Class
объект и хранится в области методов JVM. Уведомление:Класс имеет один и только одинClass
объект.
использовать отражение
Пакет java.lang.reflect
на Явеjava.lang.reflect
Пакет обеспечивает функциональность отражения.java.lang.reflect
Ни один из классов в пакетеpublic
Способ построения.
java.lang.reflect
Основные интерфейсы и классы пакета следующие:
-
Member
Интерфейс — отражает идентифицирующую информацию об одном члене (поле или методе) или конструкторе. -
Field
Класс — предоставляет информацию о полях класса и интерфейсах для доступа к полям класса. -
Method
Класс — предоставляет информацию о методах класса и интерфейсе для доступа к методам класса. -
Constructor
Класс — предоставляет информацию о конструкторе класса и интерфейсе для доступа к конструктору класса. -
Array
Класс. Этот класс предоставляет методы для динамического создания массивов JAVA и доступа к ним. -
Modifier
Класс — предоставляет статические методы и константы, класс декодирования и модификаторы доступа к членам. -
Proxy
Класс — предоставляет статические методы для динамического создания прокси-классов и экземпляров классов.
получить объект класса
Три способа получить класс:
(1)используя классforName
статический метод
Пример:
package io.github.dunwu.javacore.reflect;
public class ReflectClassDemo01 {
public static void main(String[] args) throws ClassNotFoundException {
Class c1 = Class.forName("io.github.dunwu.javacore.reflect.ReflectClassDemo01");
System.out.println(c1.getCanonicalName());
Class c2 = Class.forName("[D");
System.out.println(c2.getCanonicalName());
Class c3 = Class.forName("[[Ljava.lang.String;");
System.out.println(c3.getCanonicalName());
}
}
//Output:
//io.github.dunwu.javacore.reflect.ReflectClassDemo01
//double[]
//java.lang.String[][]
Класс объекта отражается с использованием полного имени класса. Общие сценарии приложений: этот метод обычно используется для загрузки драйверов базы данных при разработке JDBC.
(2)получить непосредственно от объектаclass
Пример:
public class ReflectClassDemo02 {
public static void main(String[] args) {
boolean b;
// Class c = b.getClass(); // 编译错误
Class c1 = boolean.class;
System.out.println(c1.getCanonicalName());
Class c2 = java.io.PrintStream.class;
System.out.println(c2.getCanonicalName());
Class c3 = int[][][].class;
System.out.println(c3.getCanonicalName());
}
}
//Output:
//boolean
//java.io.PrintStream
//int[][][]
(3)вызов объектаgetClass
метод, пример:
В классе Object есть метод getClass, потому что все классы наследуются от класса Object. Поэтому вызовите класс Object, чтобы получить
Пример:
package io.github.dunwu.javacore.reflect;
import java.util.HashSet;
import java.util.Set;
public class ReflectClassDemo03 {
enum E {A, B}
public static void main(String[] args) {
Class c = "foo".getClass();
System.out.println(c.getCanonicalName());
Class c2 = ReflectClassDemo03.E.A.getClass();
System.out.println(c2.getCanonicalName());
byte[] bytes = new byte[1024];
Class c3 = bytes.getClass();
System.out.println(c3.getCanonicalName());
Set<String> set = new HashSet<>();
Class c4 = set.getClass();
System.out.println(c4.getCanonicalName());
}
}
//Output:
//java.lang.String
//io.github.dunwu.javacore.reflect.ReflectClassDemo.E
//byte[]
//java.util.HashSet
Определить, является ли это экземпляром класса
Есть два способа определить, является ли он экземпляром класса:
- использовать
instanceof
ключевые слова -
использовать
Class
объектisInstance
метод(это родной метод)
Пример:
public class InstanceofDemo {
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
if (arrayList instanceof List) {
System.out.println("ArrayList is List");
}
if (List.class.isInstance(arrayList)) {
System.out.println("ArrayList is List");
}
}
}
//Output:
//ArrayList is List
//ArrayList is List
Создать экземпляр
Существует два основных способа создания объектов-экземпляров посредством отражения:
- использовать
Class
объектnewInstance
метод. - использовать
Constructor
объектnewInstance
метод.
Пример:
public class NewInstanceDemo {
public static void main(String[] args)
throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class<?> c1 = StringBuilder.class;
StringBuilder sb = (StringBuilder) c1.newInstance();
sb.append("aaa");
System.out.println(sb.toString());
//获取String所对应的Class对象
Class<?> c2 = String.class;
//获取String类带一个String参数的构造器
Constructor constructor = c2.getConstructor(String.class);
//根据构造器创建实例
String str2 = (String) constructor.newInstance("bbb");
System.out.println(str2);
}
}
//Output:
//aaa
//bbb
Field
Class
Object предоставляет следующие методы для получения членов объекта (Field
):
-
getFiled
- Получить публичных членов класса по имени. -
getDeclaredField
- Получить объявленных членов класса по имени. Но не может получить членов класса своего родительского класса. -
getFields
- Получить все публичные (public) члены класса. -
getDeclaredFields
- Получить всех объявленных членов класса.
Пример выглядит следующим образом:
public class ReflectFieldDemo {
class FieldSpy<T> {
public boolean[][] b = {{false, false}, {true, true}};
public String name = "Alice";
public List<Integer> list;
public T val;
}
public static void main(String[] args) throws NoSuchFieldException {
Field f1 = FieldSpy.class.getField("b");
System.out.format("Type: %s%n", f1.getType());
Field f2 = FieldSpy.class.getField("name");
System.out.format("Type: %s%n", f2.getType());
Field f3 = FieldSpy.class.getField("list");
System.out.format("Type: %s%n", f3.getType());
Field f4 = FieldSpy.class.getField("val");
System.out.format("Type: %s%n", f4.getType());
}
}
//Output:
//Type: class [[Z
//Type: class java.lang.String
//Type: interface java.util.List
//Type: class java.lang.Object
Method
Class
Объект предоставляет следующие методы для получения объекта (Method
):
-
getMethod
- Возвращает определенный метод класса или интерфейса. Первый параметр — это имя метода, а следующие параметры — это объект класса, соответствующий параметру метода. -
getDeclaredMethod
- Возвращает определенный объявленный метод класса или интерфейса. Первый параметр — это имя метода, а следующие параметры — это объект класса, соответствующий параметру метода. -
getMethods
- Возвращает все общедоступные методы класса или интерфейса, включая методы его родительских классов. -
getDeclaredMethods
- Возвращает все методы, объявленные классом или интерфейсом, включая общедоступные, защищенные, по умолчанию (пакет) и частные методы, но исключая унаследованные методы.
получить одинMethod
объект, вы можете использоватьinvoke
метод для вызова этого метода.
invoke
Прототип метода:
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
Пример:
public class ReflectMethodDemo {
public static void main(String[] args)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 返回所有方法
Method[] methods1 = System.class.getDeclaredMethods();
System.out.println("System getDeclaredMethods 清单(数量 = " + methods1.length + "):");
for (Method m : methods1) {
System.out.println(m);
}
// 返回所有 public 方法
Method[] methods2 = System.class.getMethods();
System.out.println("System getMethods 清单(数量 = " + methods2.length + "):");
for (Method m : methods2) {
System.out.println(m);
}
// 利用 Method 的 invoke 方法调用 System.currentTimeMillis()
Method method = System.class.getMethod("currentTimeMillis");
System.out.println(method);
System.out.println(method.invoke(null));
}
}
Constructor
Class
Объект предоставляет следующие методы для получения конструктора объекта (Constructor
):
-
getConstructor
- Возвращает конкретный публичный конструктор класса. Параметр — это объект класса, соответствующий параметру метода. -
getDeclaredConstructor
- Возвращает конкретный конструктор класса. Параметр — это объект класса, соответствующий параметру метода. -
getConstructors
- Возвращает все общедоступные конструкторы класса. -
getDeclaredConstructors
- Возвращает все конструкторы класса.
получить одинConstructor
объект, вы можете использоватьnewInstance
метод для создания экземпляра класса.
Пример:
public class ReflectMethodConstructorDemo {
public static void main(String[] args)
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor<?>[] constructors1 = String.class.getDeclaredConstructors();
System.out.println("String getDeclaredConstructors 清单(数量 = " + constructors1.length + "):");
for (Constructor c : constructors1) {
System.out.println(c);
}
Constructor<?>[] constructors2 = String.class.getConstructors();
System.out.println("String getConstructors 清单(数量 = " + constructors2.length + "):");
for (Constructor c : constructors2) {
System.out.println(c);
}
System.out.println("====================");
Constructor constructor = String.class.getConstructor(String.class);
System.out.println(constructor);
String str = (String) constructor.newInstance("bbb");
System.out.println(str);
}
}
Array
Массивы — это особый тип в Java, который можно присвоить ссылке на объект. Давайте рассмотрим пример создания массива с использованием отражения:
public class ReflectArrayDemo {
public static void main(String[] args) throws ClassNotFoundException {
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls, 25);
//往数组里添加内容
Array.set(array, 0, "Scala");
Array.set(array, 1, "Java");
Array.set(array, 2, "Groovy");
Array.set(array, 3, "Scala");
Array.set(array, 4, "Clojure");
//获取某一项的内容
System.out.println(Array.get(array, 3));
}
}
//Output:
//Scala
где находится класс Arrayjava.lang.reflect.Array
своего рода. мы проходимArray.newInstance
Создайте объект массива, прототипом которого является:
public static Object newInstance(Class<?> componentType, int length)
throws NegativeArraySizeException {
return newArray(componentType, length);
}
Динамический прокси
Динамические прокси — очень важный вариант использования отражения. Динамические прокси часто используются в некоторых средах Java. Например, интерфейс Spring AOP и интерфейс SPI Dubbo реализованы на основе динамического прокси-сервера Java.
статический прокси
Статический прокси фактически относится к шаблону прокси в шаблоне проектирования.
Шаблон прокси предоставляет другим объектам прокси для управления доступом к этому объекту.
SubjectОпределяет общедоступный интерфейс RealSubject и Proxy, чтобы Proxy можно было использовать везде, где используется RealSubject.
abstract class Subject {
public abstract void Request();
}
RealSubjectОпределяет реальную сущность, которую представляет Прокси.
class RealSubject extends Subject {
@Override
public void Request() {
System.out.println("真实的请求");
}
}
ProxyСохранение ссылки делает объект доступным для прокси-сервера и предоставляет интерфейс, идентичный интерфейсу субъекта, поэтому прокси-сервер можно использовать вместо объекта.
class Proxy extends Subject {
private RealSubject real;
@Override
public void Request() {
if (null == real) {
real = new RealSubject();
}
real.Request();
}
}
инструкция:
Хотя режим статического прокси имеет большие преимущества в доступе к недоступным ресурсам и расширении бизнес-функций существующего интерфейса, широкое использование таких статических прокси увеличит размер классов в нашей системе и затруднит ее обслуживание; а так как функции прокси и RealSubject по сути одинаковы, Proxy действует только как посредник Существование этого proxy в системе приводит к раздутой и рыхлой структуре системы.
Динамический прокси
Чтобы решить проблему статических прокси, есть идея создания динамических прокси:
В рабочем состоянии, когда требуется прокси, прокси создается динамически в соответствии с Subject и RealSubject.После того, как он будет израсходован, он будет уничтожен, чтобы избежать избыточной проблемы класса роли Proxy в системе. .
Динамический прокси Java основан на классическом режиме прокси и представляет InvocationHandler, который отвечает за унифицированное управление всеми вызовами методов.
Шаги динамического прокси:
- Получить список всех интерфейсов на RealSubject;
- Определяет имя класса создаваемого прокси-класса, по умолчанию:
com.sun.proxy.$ProxyXXXX
; - Динамически создавать байт-код класса Proxy в коде в соответствии с информацией об интерфейсе, который необходимо реализовать;
- Преобразуйте соответствующий байт-код в соответствующий объект класса;
- Создайте
InvocationHandler
Обработчик экземпляра, используемый для обработкиProxy
все вызовы методов; - Объект класса Proxy принимает созданный объект-обработчик в качестве параметра и создает экземпляр объекта-посредника.
Как видно из вышеизложенного, реализация динамического прокси JDK основана на способе реализации интерфейса, так что Proxy и RealSubject выполняют одну и ту же функцию.
Но на самом деле есть и другой способ мышления: через наследование. То есть: пусть Proxy наследует RealSubject, чтобы оба имели одинаковую функцию, а Proxy также мог добиться полиморфизма, переопределив методы в RealSubject. CGLIB разработан на основе этой идеи.
В динамическом прокси-механизме Java есть два важных класса (интерфейса), один из которыхInvocationHandler
интерфейс, другойProxy
Класс, этот класс и интерфейс необходимы для реализации нашего динамического прокси.
Интерфейс обработчика вызовов
InvocationHandler
Определение интерфейса:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
Каждый динамический прокси-класс должен реализовыватьInvocationHandler
Этот интерфейс и каждый экземпляр прокси-класса связаны с обработчиком.Когда мы вызываем метод через прокси-объект, вызов этого метода будет перенаправленInvocationHandler
этот интерфейсinvoke
способ вызова.
Давайте взглянем на единственный метод вызова метода интерфейса InvocationHandler:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable
Описание параметра:
- proxy- Реальный объект прокси.
-
method- Метод реального объекта, который нужно вызвать
Method
объект - args- Параметры, которые будут приняты при вызове метода реального объекта
Если вы не очень хорошо это понимаете, я объясню эти параметры более подробно на примере позже.
Прокси-класс
Proxy
Роль этого класса заключается в динамическом создании класса прокси-объектов, он предоставляет множество методов, но чаще всего мы используемnewProxyInstance
Сюда:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
Роль этого метода заключается в получении динамического прокси-объекта.
Описание параметра:
- loader- Объект ClassLoader, определяющий, какой объект ClassLoader будет загружать сгенерированный прокси-объект.
- interfaces- Массив интерфейсных объектов, представляющий собой набор интерфейсов, которые я предоставлю объекту, который мне нужно проксировать.Если я предоставлю ему набор интерфейсов, то прокси-объект объявляет о реализации интерфейса (полиморфизм), поэтому я может вызывать методы в этом наборе интерфейсов
- h- Объект InvocationHandler, указывающий, с каким объектом InvocationHandler будет связан, когда мой динамический прокси-объект вызывает метод.
Экземпляр динамического прокси
После того, как в приведенном выше содержании представлены эти два интерфейса (класса), давайте посмотрим, как выглядит наш динамический режим прокси на примере:
Сначала мы определяем интерфейс типа Subject и объявляем для него два метода:
public interface Subject {
void hello(String str);
String bye();
}
Затем для реализации этого интерфейса определяется класс, который является нашим реальным объектом, классом RealSubject:
public class RealSubject implements Subject {
@Override
public void hello(String str) {
System.out.println("Hello " + str);
}
@Override
public String bye() {
System.out.println("Goodbye");
return "Over";
}
}
Следующим шагом является определение динамического прокси-класса.Как упоминалось ранее, каждый динамический прокси-класс должен реализовывать интерфейс InvocationHandler, поэтому наш динамический прокси-класс не является исключением:
public class InvocationHandlerDemo implements InvocationHandler {
// 这个就是我们要代理的真实对象
private Object subject;
// 构造方法,给我们要代理的真实对象赋初值
public InvocationHandlerDemo(Object subject) {
this.subject = subject;
}
@Override
public Object invoke(Object object, Method method, Object[] args)
throws Throwable {
// 在代理真实对象前我们可以添加一些自己的操作
System.out.println("Before method");
System.out.println("Call Method: " + method);
// 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
Object obj = method.invoke(subject, args);
// 在代理真实对象后我们也可以添加一些自己的操作
System.out.println("After method");
System.out.println();
return obj;
}
}
Наконец, давайте взглянем на наш класс Client:
public class Client {
public static void main(String[] args) {
// 我们要代理的真实对象
Subject realSubject = new RealSubject();
// 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的
InvocationHandler handler = new InvocationHandlerDemo(realSubject);
/*
* 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数
* 第一个参数 handler.getClass().getClassLoader() ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象
* 第二个参数realSubject.getClass().getInterfaces(),我们这里为代理对象提供的接口是真实对象所实行的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了
* 第三个参数handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上
*/
Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject
.getClass().getInterfaces(), handler);
System.out.println(subject.getClass().getName());
subject.hello("World");
String result = subject.bye();
System.out.println("Result is: " + result);
}
}
Давайте сначала посмотрим на вывод консоли:
com.sun.proxy.$Proxy0
Before method
Call Method: public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String)
Hello World
After method
Before method
Call Method: public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye()
Goodbye
After method
Result is: Over
Давайте сначала посмотримcom.sun.proxy.$Proxy0
Эта штука, мы видим, эта штука сделанаSystem.out.println(subject.getClass().getName());
Это утверждение напечатано, тогда почему мы вернули имя этого объекта агента?
Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject
.getClass().getInterfaces(), handler);
Возможно, я думал, что возвращаемый прокси-объект будет объектом типа Subject или объектом InvocationHandler, но это не так.Почему мы можем преобразовать его в объект типа Subject здесь?
Причина в следующем: во втором параметре метода newProxyInstance мы предоставляем набор интерфейсов для этого прокси-объекта, тогда мой прокси-объект будет реализовывать этот набор интерфейсов.В это время мы, конечно, можем заставить прокси-объект ввести тип Convert к любому из этого набора интерфейсов, потому что интерфейс здесь имеет тип Subject, поэтому его можно преобразовать в тип Subject.
В то же время мы должны помнить, что черезProxy.newProxyInstance
Созданный прокси-объект — это объект, который динамически генерируется при запуске jvm.Это не наш тип InvocationHandler и не тип определенного нами набора интерфейсов, а объект, который динамически генерируется во время выполнения и именуется таким же образом. В этой форме начните с $, прокси находится посередине, а последняя цифра представляет собой метку объекта.
Давайте посмотрим на эти два предложения
subject.hello("World");
String result = subject.bye();
Вот метод в интерфейсе, реализованный прокси-объектом.В это время программа перейдет к методу вызова в обработчике, связанном с прокси-объектом для выполнения, и наш объект-обработчик принимает RealSubject Параметр типа указывает, что то, что я хочу проксировать, является реальным объектом, поэтому в это время будет вызван метод вызова в обработчике для выполнения.
Мы видим, что когда мы на самом деле вызываем метод реального объекта через прокси-объект, мы можем добавить некоторые наши собственные операции до и после метода, и мы видим, что наш объект метода выглядит так:
public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String)
public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye()
Это два метода в нашем интерфейсе Subject, что доказывает, что когда я вызываю метод через прокси-объект, я фактически делегирую вызов из метода вызова объекта-обработчика, с которым он связан, а не от себя. вызов вызывается прокси.
резюме
приложение для отражения
использованная литература
- Идеи программирования на Java
- Основные технологии JAVA (том 1)
- Углубленный анализ отражения Java (1) — основы
- Основы Java — отражение (очень важно)
- Официальная документация по API Reflection
- Подробное объяснение механизма динамического прокси в Java.
- Подробное объяснение механизма динамического прокси Java (JDK и CGLIB, Javassist, ASM)