1. 概述
大力模式的设计冬季是通过代理对象来访问真实对象,通过建立一个对象代理类,由代理对象控制原对象的引用,从而实现对真实对象的操作。
在代理模式中,代理对象主要起一个中介的作用,用于协调与连接调用者(即客户端)和被调用者(即目标对象),在一定程度上降低了系统的耦合度,同时也保护了目标对象。但缺点是在调用者与被调用者之间增加了代理对象,可能会造成请求的处理速度变慢。
2. 静态代理
2.1 UML图
Subject: 抽象角色,声明了真实对象和代理对象的共同接口;
Proxy: 代理角色,实现了与真实对象相同的接口,所以在任何时刻都能够代理真实对象,并且代理对象内部包含了真实对象的引用,所以它可以操作真是对象,同时也可以附加其他的操作,相当于对真实对象进行封装;
RealSubject: 真实对象,是我们最终要引用的对象;
2.2 代码与测试
3. 动态代理
从静态代理模式的示例代码可以发现,静态代理的缺点显而易见,那就是当真实类的方法越来越多的时候,这样构建的代理类的代码量是非常大的,所以就产生了动态代理。
动态代理使用单个类(代理类)为具有任意数量方法的任意类(真实类)的多个方法调用提供服务。看到这里,就可以联想到动态代理的实现与反射密不可分。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象的方法的功能称为JAVA语言的反射机制。
3.1 JDK 动态代理(接口代理)
(1)UML图
(2)JDK 动态代理用法
JDK代理涉及到 java.lang.reflect 包中的 InvocationHandler 接口和 Proxy 类,核心方法是
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
JDK动态代理过程中实际上代理的是接口,是因为在创建代理实例的时候,依赖的是 java.lang.reflect 包中 Proxy 类的 newProxyInstance 方法,该方法的生效恰恰需要这个参数。
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {
Objects.requireNonNull(h);
final Class<?> caller = System.getSecurityManager() == null
? null
: Reflection.getCallerClass();
/*
* Look up or generate the designated proxy class and its constructor.
*/
Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
return newProxyInstance(caller, cons, h);
}
在JDK动态代理中,实现了 InvocationHandler 接口的类可以看作是 代理类(因为类也是一种对象,所以为了面描述关系把代理类形容成了代理对象)。JDK动态代理就是围绕实现了 InvocationHandler 的代理类进行的。比如下面的类就是一个 InvocationHandler 的实现类,同时它也是一个代理类。
package com.pattern.design.proxy.jdkproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class JDKDynamicProxy implements InvocationHandler {
private Object target;
public JDKDynamicProxy(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
start();
Object result = method.invoke(target, args);
end();
return result;
}
public void start() {
System.out.println("--------- 执行了 start -----------");
}
public void end() {
System.out.println("---------- 执行了 end -----------");
}
}
代理类一个最重要的方法就是 invoke() 方法,它有3个参数:
- Object proxy: 动态代理对象;
- Method method: 表示最终要执行的方法,method.invoke() 用于执行被代理的方法,也就是真正的目标方法;
- Object[] args: 这个参数就是向目标方法传递参数;
我们构造好了代理类,现在就要使用它来实现我们对目标对象的调用,那么如何操作呢?
目标对象
// 接口
public interface Person {
void wakeup();
void sleep();
}
// 目标类
public class Student implements Person {
private String name;
public Student(String name) {
this.name = name;
}
@Override
public void wakeup() {
System.out.println("学生" + name + "醒了");
}
@Override
public void sleep() {
System.out.println("学生" + name + "睡了");
}
}
使用代理类
package com.pattern.design.proxy.jdkproxy;
import com.pattern.design.proxy.jdkproxy.JDKDynamicProxy;
import com.pattern.design.proxy.jdkproxy.Person;
import com.pattern.design.proxy.jdkproxy.Student;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class JDKDynamicProxyTest {
public static void main(String[] args) {
ClassLoader classLoader = JDKDynamicProxy.class.getClassLoader();
Class<?>[] interfaces = Student.class.getInterfaces();
InvocationHandler handler = new JDKDynamicProxy(new Student("张三"));
Object obj = Proxy.newProxyInstance(classLoader, interfaces, handler);
System.out.println(obj.getClass());
Person proxy = (Person) obj;
proxy.wakeup();
System.out.println("==========================");
proxy.sleep();
}
}
如果要用 JDK 动态代理的话,就需要知道目标对象的 类加载器、目标对象的接口、目标对象。构造完成后,我们就可以调用 Proxy.newProxyInstance 方法,然后把类加载器、目标对象的接口、目标对象绑定上去就行了。
这里需要注意一下 Proxy 类,它就是动态代理实际用到的代理类。
(3)JDK 动态代理原理
JDK的动态代理为什么需要被代理类一定要实现至少一个接口?
Proxy 位于 java.lang.reflect 包下,这同时也表明了动态代理的本质就是反射。
我们知道,JDK的动态代理是在运行过程中生成了新的代理类,通过代理类来执行了原对象的方法。要分析动态代理运行原理,就需要获取生成的代理类来进行分析。
可以通过如下的代码产生代理类的字节码文件:
public static void main(String[] args) {
String proxyName = "com.design.pattern.study.$Proxy0";
Class<?>[] interfaces = new Class[]{Person.class};
int accessFlags = Modifier.PUBLIC;
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
// 将字节数组写出到磁盘
File file = new File("$Proxy0.class");
try {
OutputStream outputStream = new FileOutputStream(file);
outputStream.write(proxyClassFile);
} catch (Exception e) {
e.printStackTrace();
}
}
或者
public static void main(String[] args) {
byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy", new Class[]{Person.class});
try (
FileOutputStream fos = new FileOutputStream(new File("$Proxy.class"));
) {
fos.write(bytes);
fos.flush();
} catch (Exception e) {
e.printStackTrace();
}
}
生成的字节码文件是$Proxy0.class,IDEA直接打开字节码文件,它会自动将字节码文件翻译成JAVA代码。
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.design.pattern.study;
import com.design.pattern.proxy.jdkproxy.person.Person;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public class $Proxy0 extends Proxy implements Person {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m4;
private static Method m0;
public $Proxy0(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 void wakeup() 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 {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
public final void sleep() throws {
try {
super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
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);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("com.design.pattern.proxy.jdkproxy.person.Person").getMethod("wakeup");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("com.design.pattern.proxy.jdkproxy.person.Person").getMethod("sleep");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
- 通过源码我们发现,
$Proxy0类继承了 Proxy 类,同时实现了 Person 接口。到这里,我们就能解释为什么JDK的动态代理一定需要被代理类实现一个接口而不能通过继承来实现这个问题了:因为JAVA中不支持多继承,而JDK的动态代理再创建代理对象时,默认让代理对象继承了Proxy类,所以JDK只能通过接口去实现动态代理。$Proxy0实现了Person接口,所以重写了接口中的两个方法($Proxy0同时还重写了Object类中的 equals()、hashCode()、toString() 方法)。所以当我们调用 wakeup() 方法时,先是调用 $Proxy0.wakeup() 方法,在这个方法中直接调用了 super.h.invoke() 方法,父类是 Proxy,父类中的h就是我们定义的 InvocationHandler(在调用Proxy.newProxyInstance()方法时将实现了InvocationHandler接口的代理类的对象设置进去了),所以这会儿调用到 JDKDynamicProxy 的 invoke() 方法时,会先经过 InvocationHandler 的 invoke() 方法,然后再通过反射 method,invoke() 去调用目标对象的方法,因此每次都会先执行 start() 方法,目标方法执行完后又会执行 end() 方法。
3.2 CGLIB 动态代理
从上面可以看出,JDK动态代理的前提条件是要有接口存在,那还有许多场景是没有接口的,这个时候就需要 cglib 动态代理了。
CGLIB(Code Generation Library)是一个基于ASM的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。
CGLIB通过继承方式实现代理,CGLIB动态代理过程中生成的是实现类的子类。
(1) UML图
(2) 代码示例
package com.design.pattern.proxy.cglibproxy;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 所需的代理类
*
* @author albert.tan
* @date 2022/11/28
*/
public class CglibProxy implements MethodInterceptor {
private Enhancer enhancer = new Enhancer();
private Object target;
public CglibProxy(Object target) {
this.target = target;
}
public Object getProxy() {
// 设置所需创建子类的类
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
start();
Object result = method.invoke(target, objects);
end();
return result;
}
public void start() {
System.out.println("Hello, Welcome");
}
public void end() {
System.out.println("Bye Bye");
}
}
测试类
package com.design.pattern.proxy.cglibproxy;
import com.design.pattern.proxy.jdkproxy.person.Student;
/**
* @author albert.tan
* @date 2022/11/28
*/
public class TestCglibProxy {
public static void main(String[] args) {
CglibProxy proxy = new CglibProxy(new Student("张三"));
Student student = (Student) proxy.getProxy();
student.wakeup();
student.sleep();
}
}
(3) CGLIB 动态代理原理
通过设置如下的代码将cglib动态生成的字节码写入到cglib文件夹下
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib");
public class TestCglibProxy {
public static void main(String[] args) {
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "cglib");
CglibProxy proxy = new CglibProxy(new Student("张三"));
Student student = (Student) proxy.getProxy();
student.wakeup();
student.sleep();
}
}
cglib生成的字节码如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.design.pattern.proxy.jdkproxy.person;
import java.lang.reflect.Method;
import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Factory;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class Student$$EnhancerByCGLIB$$8c92fe4a extends Student implements Factory {
private boolean CGLIB$BOUND;
public static Object CGLIB$FACTORY_DATA;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static Object CGLIB$CALLBACK_FILTER;
private static final Method CGLIB$wakeup$0$Method;
private static final MethodProxy CGLIB$wakeup$0$Proxy;
private static final Object[] CGLIB$emptyArgs;
private static final Method CGLIB$sleep$1$Method;
private static final MethodProxy CGLIB$sleep$1$Proxy;
private static final Method CGLIB$equals$2$Method;
private static final MethodProxy CGLIB$equals$2$Proxy;
private static final Method CGLIB$toString$3$Method;
private static final MethodProxy CGLIB$toString$3$Proxy;
private static final Method CGLIB$hashCode$4$Method;
private static final MethodProxy CGLIB$hashCode$4$Proxy;
private static final Method CGLIB$clone$5$Method;
private static final MethodProxy CGLIB$clone$5$Proxy;
static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
CGLIB$emptyArgs = new Object[0];
Class var0 = Class.forName("com.design.pattern.proxy.jdkproxy.person.Student$$EnhancerByCGLIB$$8c92fe4a");
Class var1;
Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
CGLIB$equals$2$Method = var10000[0];
CGLIB$equals$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$2");
CGLIB$toString$3$Method = var10000[1];
CGLIB$toString$3$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$3");
CGLIB$hashCode$4$Method = var10000[2];
CGLIB$hashCode$4$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$4");
CGLIB$clone$5$Method = var10000[3];
CGLIB$clone$5$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$5");
var10000 = ReflectUtils.findMethods(new String[]{"wakeup", "()V", "sleep", "()V"}, (var1 = Class.forName("com.design.pattern.proxy.jdkproxy.person.Student")).getDeclaredMethods());
CGLIB$wakeup$0$Method = var10000[0];
CGLIB$wakeup$0$Proxy = MethodProxy.create(var1, var0, "()V", "wakeup", "CGLIB$wakeup$0");
CGLIB$sleep$1$Method = var10000[1];
CGLIB$sleep$1$Proxy = MethodProxy.create(var1, var0, "()V", "sleep", "CGLIB$sleep$1");
}
final void CGLIB$wakeup$0() {
super.wakeup();
}
public final void wakeup() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$wakeup$0$Method, CGLIB$emptyArgs, CGLIB$wakeup$0$Proxy);
} else {
super.wakeup();
}
}
final void CGLIB$sleep$1() {
super.sleep();
}
public final void sleep() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$sleep$1$Method, CGLIB$emptyArgs, CGLIB$sleep$1$Proxy);
} else {
super.sleep();
}
}
final boolean CGLIB$equals$2(Object var1) {
return super.equals(var1);
}
public final boolean equals(Object var1) {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
Object var2 = var10000.intercept(this, CGLIB$equals$2$Method, new Object[]{var1}, CGLIB$equals$2$Proxy);
return var2 == null ? false : (Boolean)var2;
} else {
return super.equals(var1);
}
}
final String CGLIB$toString$3() {
return super.toString();
}
public final String toString() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? (String)var10000.intercept(this, CGLIB$toString$3$Method, CGLIB$emptyArgs, CGLIB$toString$3$Proxy) : super.toString();
}
final int CGLIB$hashCode$4() {
return super.hashCode();
}
public final int hashCode() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
Object var1 = var10000.intercept(this, CGLIB$hashCode$4$Method, CGLIB$emptyArgs, CGLIB$hashCode$4$Proxy);
return var1 == null ? 0 : ((Number)var1).intValue();
} else {
return super.hashCode();
}
}
final Object CGLIB$clone$5() throws CloneNotSupportedException {
return super.clone();
}
protected final Object clone() throws CloneNotSupportedException {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
return var10000 != null ? var10000.intercept(this, CGLIB$clone$5$Method, CGLIB$emptyArgs, CGLIB$clone$5$Proxy) : super.clone();
}
public static MethodProxy CGLIB$findMethodProxy(Signature var0) {
String var10000 = var0.toString();
switch (var10000.hashCode()) {
case -1385928386:
if (var10000.equals("sleep()V")) {
return CGLIB$sleep$1$Proxy;
}
break;
case -508378822:
if (var10000.equals("clone()Ljava/lang/Object;")) {
return CGLIB$clone$5$Proxy;
}
break;
case 391780310:
if (var10000.equals("wakeup()V")) {
return CGLIB$wakeup$0$Proxy;
}
break;
case 1826985398:
if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
return CGLIB$equals$2$Proxy;
}
break;
case 1913648695:
if (var10000.equals("toString()Ljava/lang/String;")) {
return CGLIB$toString$3$Proxy;
}
break;
case 1984935277:
if (var10000.equals("hashCode()I")) {
return CGLIB$hashCode$4$Proxy;
}
}
return null;
}
public Student$$EnhancerByCGLIB$$8c92fe4a() {
CGLIB$BIND_CALLBACKS(this);
}
public Student$$EnhancerByCGLIB$$8c92fe4a(String var1) {
super(var1);
CGLIB$BIND_CALLBACKS(this);
}
public static void CGLIB$SET_THREAD_CALLBACKS(Callback[] var0) {
CGLIB$THREAD_CALLBACKS.set(var0);
}
public static void CGLIB$SET_STATIC_CALLBACKS(Callback[] var0) {
CGLIB$STATIC_CALLBACKS = var0;
}
private static final void CGLIB$BIND_CALLBACKS(Object var0) {
Student$$EnhancerByCGLIB$$8c92fe4a var1 = (Student$$EnhancerByCGLIB$$8c92fe4a)var0;
if (!var1.CGLIB$BOUND) {
var1.CGLIB$BOUND = true;
Object var10000 = CGLIB$THREAD_CALLBACKS.get();
if (var10000 == null) {
var10000 = CGLIB$STATIC_CALLBACKS;
if (var10000 == null) {
return;
}
}
var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
}
}
public Object newInstance(Callback[] var1) {
CGLIB$SET_THREAD_CALLBACKS(var1);
Student$$EnhancerByCGLIB$$8c92fe4a var10000 = new Student$$EnhancerByCGLIB$$8c92fe4a();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}
public Object newInstance(Callback var1) {
CGLIB$SET_THREAD_CALLBACKS(new Callback[]{var1});
Student$$EnhancerByCGLIB$$8c92fe4a var10000 = new Student$$EnhancerByCGLIB$$8c92fe4a();
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}
public Object newInstance(Class[] var1, Object[] var2, Callback[] var3) {
CGLIB$SET_THREAD_CALLBACKS(var3);
Student$$EnhancerByCGLIB$$8c92fe4a var10000 = new Student$$EnhancerByCGLIB$$8c92fe4a;
switch (var1.length) {
case 0:
var10000.<init>();
break;
case 1:
if (var1[0].getName().equals("java.lang.String")) {
var10000.<init>((String)var2[0]);
break;
}
throw new IllegalArgumentException("Constructor not found");
default:
throw new IllegalArgumentException("Constructor not found");
}
CGLIB$SET_THREAD_CALLBACKS((Callback[])null);
return var10000;
}
public Callback getCallback(int var1) {
CGLIB$BIND_CALLBACKS(this);
MethodInterceptor var10000;
switch (var1) {
case 0:
var10000 = this.CGLIB$CALLBACK_0;
break;
default:
var10000 = null;
}
return var10000;
}
public void setCallback(int var1, Callback var2) {
switch (var1) {
case 0:
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var2;
default:
}
}
public Callback[] getCallbacks() {
CGLIB$BIND_CALLBACKS(this);
return new Callback[]{this.CGLIB$CALLBACK_0};
}
public void setCallbacks(Callback[] var1) {
this.CGLIB$CALLBACK_0 = (MethodInterceptor)var1[0];
}
static {
CGLIB$STATICHOOK1();
}
}
4. 常见问题
4.1 JDK 动态代理
(1) 为什么说JDK的动态代理要给予接口实现,而不能给予继承来实现?
(2) 在JDK的动态代理中,在目标对象内部调用自己的另一个方法时,另一个方法在执行时,为什么没有经过代理对象?
在目标对象内部调用自己的另一个方法时,实际上调用的是this的方法,而此时的this指向的是目标对象,并不是 $Proxy0 这个代理对象,只有在调用代理对象的方法时,才会经过 InvocationHandler.invoke() 方法。所以在目标对象内部调用自己的另一个方法时,另一个方法执行时不会经过代理对象。
Spring中@Transactional声明事务时,出现了事务不生效的情况就是因为是内部调用,并没有经过代理对象去调用,从而导致了事务的失效。
同样的案例还有 @Async 等案例,在同一个类的两个方法中开启异步,然后在方法内部进行相互调用,最终导致异步失效的问题。
4.2 CGLIB 动态代理
5. 代理模式与装饰器模式的区别
6. 运用与实例
参考文献
[1] https://juejin.cn/post/6844903951976890381
[2] https://blog.csdn.net/a745233700/article/details/83629577
[3] https://blog.csdn.net/weixin_43953283/article/details/125783249
[4] https://blog.csdn.net/qq_46494427/article/details/124173617

224

被折叠的 条评论
为什么被折叠?



