Когда мы читаем исходный код некоторых фреймворков Java, мы часто видим использование динамического прокси-механизма, который может улучшить существующий код без ощущения, так что код имеет лучшую масштабируемость. Анализ данной статьи проводится через статический прокси, динамический прокси JDK, динамический прокси CGLIB.
статический прокси
Статический прокси — это байт-код класса прокси перед запуском программы..classОн был скомпилирован, обычно статический прокси-класс прокси-сервер только одного целевого класса, и прокси-класс, и целевой класс реализуют один и тот же интерфейс.
Далее, давайте проанализируем, что такое статический прокси, сначала с помощью демо, а сейчас создадим интерфейс Animal, который содержитcallфункция.
package top.ytao.demo.proxy;
/**
* Created by YangTao
*/
public interface Animal {
void call();
}
Создайте целевой класс Cat и реализуйте интерфейс Animal Ниже приведена реализация Cat, издающего звук.
package top.ytao.demo.proxy;
/**
* Created by YangTao
*/
public class Cat implements Animal {
@Override
public void call() {
System.out.println("喵喵喵 ~");
}
}
Поскольку Cat голоден до вызова, нам нужно вызвать его в целевом методе.Cat#callПредыдущее описание — голодание, то есть использование статического прокси-сервера для достижения кошачьего голодания, а затем мяуканье.
package top.ytao.demo.proxy.jdk;
import top.ytao.demo.proxy.Animal;
/**
* Created by YangTao
*/
public class StaticProxyAnimal implements Animal {
private Animal impl;
public StaticProxyAnimal(Animal impl) {
this.impl = impl;
}
@Override
public void call() {
System.out.println("猫饥饿");
impl.call();
}
}
Кошачий голод и лай реализуются путем вызова статических прокси.
public class Main {
@Test
public void staticProxy(){
Animal staticProxy = new StaticProxyAnimal(new Cat());
staticProxy.call();
}
}
Результаты
Отношения между прокси-классом, целевым классом и интерфейсом показаны на рисунке:
Приведенное выше содержимое показывает, что прокси-класс реализует статический прокси, удерживая объект целевого класса и затем вызывая метод целевого класса. Хотя статический прокси реализует прокси, в некоторых случаях он имеет очевидные недостатки:
- Когда мы добавляем метод в интерфейс Animal, не только реализация класса Cat должна добавить реализацию этого метода, но в то же время, поскольку прокси-класс реализует интерфейс Animal, прокси-класс также должен реализовывать новый метод Animal, который относительно велик для проекта, когда он большой, он не очень удобен в обслуживании.
- Реализация прокси-класса
Animal#callОн установлен для объекта целевого класса Cat.Если вам нужно добавить прокси целевого класса Dog, вы должны реализовать соответствующий прокси-класс для класса Dog, что делает повторное использование прокси-класса недружественным и чрезмерным. прокси-класс также сложен в обслуживании.
Вышеуказанные проблемы решаются мирным путем в динамическом прокси JDk.
Динамический прокси JDK
Основное различие между динамическим прокси-классом и статическим прокси-классом заключается в том, что байт-код прокси-класса не генерируется до запуска программы, а автоматически создается программой в виртуальной машине при запуске программы. Продолжайте реализовывать динамический прокси JDK с классом Cat и интерфейсом Animal выше.
Реализовать интерфейс InvocationHandler.
Динамический прокси-класс JDK должен реализовать интерфейс java.lang.reflect.InvocationHandler в пакете отражения, и в этом интерфейсе есть только один метод вызова:
существуетInvocationHandler#invokerДолжен быть вызван прокси-метод целевого класса, иначе реализация прокси не может быть достигнута. Ниже приведен код для реализации InvocationHandler.
/**
* Created by YangTao
*/
public class TargetInvoker implements InvocationHandler {
// 代理中持有的目标类
private Object target;
public TargetInvoker(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("jdk 代理执行前");
Object result = method.invoke(target, args);
System.out.println("jdk 代理执行后");
return result;
}
}
в реализацииInvocationHandler#invoker, метод имеет три параметра:
- proxy Прокси-объект целевого прокси-объекта, который является реальным прокси-объектом.
- метод выполняет метод целевого класса
- args Аргументы для выполнения метода целевого класса.
Создайте динамический прокси-класс JDK.
Создание экземпляра класса динамического прокси JDK также создается с использованием класса java.lang.reflect.Proxy в пакете отражения. позвонивProxy#newProxyInstanceСоздайте статический метод.
/**
*
* Created by YangTao
*/
public class DynamicProxyAnimal {
public static Object getProxy(Object target) throws Exception {
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 指定目标类的类加载
target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个,这是一个数组
new TargetInvoker(target) // 代理对象处理器
);
return proxy;
}
}
Proxy#newProxyInstanceТри параметра в (загрузчик ClassLoader, интерфейсы Class>[], InvocationHandler h):
- загрузчик класса loader, который загружает прокси-объекты
- interfaces Интерфейс, реализуемый прокси-объектом, который реализует тот же интерфейс, что и целевой объект.
- h Обработчик, который обрабатывает логику прокси-объекта, то есть класс реализации InvocationHandler выше.
Наконец реализуем реализацию динамического прокси DynamicProxyAnimal:
public class Main {
@Test
public void dynamicProxy() throws Exception {
Cat cat = new Cat();
Animal proxy = (Animal) DynamicProxyAnimal.getProxy(cat);
proxy.call();
}
}
Результаты:
В приведенном выше коде есть два вопроса: как создается прокси-класс и как прокси-класс вызывает методы?
анализировать
отProxy#newProxyInstanceЗапись для анализа исходного кода:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
// 查找或生成指定的代理类
Class<?> cl = getProxyClass0(loader, intfs);
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
// 获取代理的构造器
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
// 处理代理类修饰符,使得能被访问
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 创建代理类实例化
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
Прокси-класс получается в методе newProxyInstance.Если функция класса недоступна, к ней можно получить доступ, и, наконец, прокси-класс создается. Ядром этого кода является получение прокси-классаgetProxyClass0метод.
private static final WeakCache<ClassLoader, Class<?>[], Class<?>> proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
// 实现类的接口不能超过 65535 个
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// 获取代理类
return proxyClassCache.get(loader, interfaces);
}
Если указанный прокси-класс существует в кеше proxyClassCache, он будет получен непосредственно из кеша, если его нет, то прокси-класс будет создан через ProxyClassFactory. Что касается того, почему максимальный интерфейс равен 65535, это определяется файловой структурой байт-кода и виртуальной машиной Java, что можно понять, изучив файл байт-кода.
ВходитьproxyClassCache#get, чтобы получить прокси-класс:
продолжать вводитьFactory#getПроверять,
наконец, чтобыProxyClassFactory#apply, где реализовано создание прокси-класса.
private static final class ProxyClassFactory implements BiFunction<ClassLoader, Class<?>[], Class<?>>{
// 所有代理类名称都已此前缀命名
private static final String proxyClassNamePrefix = "$Proxy";
// 代理类名的编号
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
for (Class<?> intf : interfaces) {
// 校验代理和目标对象是否实现同一接口
Class<?> interfaceClass = null;
try {
interfaceClass = Class.forName(intf.getName(), false, loader);
} catch (ClassNotFoundException e) {
}
if (interfaceClass != intf) {
throw new IllegalArgumentException(
intf + " is not visible from class loader");
}
// 校验 interfaceClass 是否为接口
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException(
interfaceClass.getName() + " is not an interface");
}
// 判断当前 interfaceClass 是否被重复
if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
throw new IllegalArgumentException(
"repeated interface: " + interfaceClass.getName());
}
}
// 代理类的包名
String proxyPkg = null;
int accessFlags = Modifier.PUBLIC | Modifier.FINAL;
// 记录非 public 修饰符代理接口的包,使生成的代理类与它在同一个包下
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
// 获取接口类名
String name = intf.getName();
// 去掉接口的名称,获取所在包的包名
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}
if (proxyPkg == null) {
// 如果接口类是 public 修饰,则用 com.sun.proxy 包名
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
// 创建代理类名称
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
// 生成代理类字节码文件
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
// 加载字节码,生成指定代理对象
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}
}
Выше описан процесс создания байт-кода.Путем проверки свойств интерфейса определяются имя пакета и правила имен, сгенерированные файлом байт-кода прокси-класса, а затем загружается байт-код для получения экземпляра прокси. Операция генерирует файл байт-кода вProxyGenerator#generateProxyClassКонкретный файл байт-кода создается в файле, и операция с байт-кодом здесь подробно не объясняется.
Для сгенерированного файла байт-кода мы можем декомпилировать и просмотреть информацию о классе, сохранив ее локально.Существует два способа сохранить сгенерированный файл байт-кода: установка параметров jvm или запись сгенерированного byte[] в файл.
на фото вышеProxyGenerator#generateProxyClassИзвестно, что метод управляется значением атрибута saveGeneratedFiles Источник значения этого атрибута:
private static final boolean saveGeneratedFiles = ((Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"))).booleanValue();
Поэтому сохраните сгенерированный байт-код прокси-класса локально, установив.
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
Декомпилируйте и просмотрите сгенерированный прокси-класс:
Сгенерированный прокси-класс наследует Proxy и реализует интерфейс Animal, вызываяcallМетод реализуется путем вызова InvocationHandler, удерживаемого прокси-сервером.TargetInvoker#invokerисполнение.
Динамический прокси CGLIB
Механизм реализации динамического прокси CGLIB заключается в генерации подкласса целевого класса, что реализуется путем вызова метода родительского класса (целевого класса), а затем прокси расширяется при вызове метода родительского класса.
Реализовать интерфейс MethodInterceptor
По сравнению с реализацией динамического прокси JDK, динамический прокси CGLIB не требует реализации того же интерфейса, что и целевой класс, а реализует прокси через метод перехвата.Код реализован следующим образом.Во-первых, метод перехватывает интерфейс net.sf .cglib.proxy.MethodInterceptor.
/**
* Created by YangTao
*/
public class TargetInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("CGLIB 调用前");
Object result = proxy.invokeSuper(obj, args);
System.out.println("CGLIB 调用后");
return result;
}
}
Метод целевого класса вызывается через интерфейс перехвата метода, а затем перехваченный метод расширяется.Метод перехвата, который реализует интерфейс перехватчика метода, имеет четыре параметра:
- Объект прокси-класса obj
- method Метод, который в данный момент перехватывается прокси.
- args Аргументы метода перехвата
- proxy Метод прокси класса прокси, соответствующий целевому классу.
Создайте динамический прокси-класс CGLIB
Создание класса динамического прокси CGLIB создается с использованием класса net.sf.cglib.proxy.Enhancer, который является базовым классом в классе динамического прокси CGLIB.Сначала создайте простой класс прокси:
/**
* Created by YangTao
*/
public class CglibProxy {
public static Object getProxy(Class<?> clazz){
Enhancer enhancer = new Enhancer();
// 设置类加载
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new TargetInterceptor());
// 创建代理类
return enhancer.create();
}
}
Прокси-класс можно реализовать, задав информацию о прокси-классе и логику выполнения обратного вызова метода, перехваченного прокси-классом. Реализуйте вызов динамического прокси CGLIB:
public class Main {
@Test
public void dynamicProxy() throws Exception {
Animal cat = (Animal) CglibProxy.getProxy(Cat.class);
cat.call();
}
}
Результаты:
Таким образом реализовано простое применение динамического прокси CGLIB, но в процессе использования Enhancer он обычно используется и имеет специальные функции, а также использование фильтра обратного вызова CallbackFilter, который может выборочно выполнять перехват метода при перехвате метода. целевого объекта, то есть Улучшенная обработка выбранных прокси-методов. Для использования этой функции необходимо реализовать интерфейс net.sf.cglib.proxy.CallbackFilter. Теперь добавим реализацию перехвата метода:
/**
* Created by YangTao
*/
public class TargetInterceptor2 implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("CGLIB 调用前 TargetInterceptor2");
Object result = proxy.invokeSuper(obj, args);
System.out.println("CGLIB 调用后 TargetInterceptor2");
return result;
}
}
Затем добавьте метод хобби в Cat, потому что прокси CGLIB не нуждается в реализации интерфейса и может напрямую проксировать обычный класс, поэтому нет необходимости добавлять методы в интерфейс Animal:
package top.ytao.demo.proxy;
/**
* Created by YangTao
*/
public class Cat implements Animal {
@Override
public void call() {
System.out.println("喵喵喵 ~");
}
public void hobby(){
System.out.println("fish ~");
}
}
Реализация фильтра обратного вызова CallbackFilter
/**
* Created by YangTao
*/
public class TargetCallbackFilter implements CallbackFilter {
@Override
public int accept(Method method) {
if ("hobby".equals(method.getName()))
return 1;
else
return 0;
}
}
Чтобы продемонстрировать вызов другого перехватчика метода, в настройках Enhancer используйтеEnhancer#setCallbacksУстановите несколько перехватчиков методов, параметр представляет собой массив,TargetCallbackFilter#acceptВозвращаемое число является индексом массива, который определяет вызываемый селектор обратного вызова.
/**
* Created by YangTao
*/
public class CglibProxy {
public static Object getProxy(Class<?> clazz){
Enhancer enhancer = new Enhancer();
enhancer.setClassLoader(clazz.getClassLoader());
enhancer.setSuperclass(clazz);
enhancer.setCallbacks(new Callback[]{new TargetInterceptor(), new TargetInterceptor2()});
enhancer.setCallbackFilter(new TargetCallbackFilter());
return enhancer.create();
}
}
Логика реализована по коду, метод call вызовет класс TargetInterceptor, а класс хобби вызовет класс TargetInterceptor2 Результат выполнения:
Принцип реализации CGLIB заключается в том, чтобы установить информацию о классе прокси для Enhancer, а затем использовать информацию о конфигурации в Enhancer.Enhancer#createСоздайте объект прокси-класса. Сгенерированный класс создается с помощью ASM, и эта статья не посвящена анализу. Если не обращать внимания на принцип работы ASM, относительно легко понять только принцип обработки CGLIB. Здесь мы в основном смотрим на сгенерированный файл байт-кода прокси-класса, установив
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:\\xxx");
Сгенерированные байты можно сохранить в папку F:\xxx.
Это можно увидеть путем декомпиляции
Прокси-класс наследует целевой класс Cat и одновременно загружает два перехватчика метода в прокси-класс, различает их по нижнему индексу Callbacks в качестве суффикса имени переменной и, наконец, вызывает перехват в указанном перехватчике метода для реализации окончательного выполнения. Результат прокси. Здесь следует отметить, что динамические прокси CGLIB не могут проксировать окончательные оформленные классы и методы.
Наконец
Декомпилировав сгенерированные прокси-классы JDK и прокси-классы CGLIB, мы можем увидеть реализацию их двух разных механизмов: Динамический прокси-сервер JDK реализует интерфейс целевого класса, а затем передает целевой класс в качестве параметра при построении динамического прокси, так что прокси-объект содержит целевой объект, а затем реализует операцию динамического прокси через InvocationHandler из прокси-объект. Динамический прокси-сервер CGLIB должен настроить информацию о целевом классе, а затем использовать структуру байт-кода ASM для создания подкласса целевого класса. Когда вызывается прокси-метод, прокси-операция реализуется путем перехвата метода. Как правило, динамический прокси JDK использует интерфейс для реализации прокси, а динамический прокси CGLIB использует наследование для реализации прокси.
Динамические агенты очень распространены в Java-разработке.Они широко используются в логировании, мониторинге и транзакциях.В то же время они также используются в основных компонентах большинства основных фреймворков.Освойте его основные моменты, независимо от того, разрабатываете ли вы или читаете другие фреймворки , Требуется исходный код.
личный блог: ytao.top
Обратите внимание на паблик [ytao], больше оригинальных хороших статей