Java 动态代理详解
本文内容纲要:
-代理模式
-静态代理
-JDK动态代理
-CGLIB动态代理
-JDK动态代理与CGLIB动态代理对比
-面试题
-后记
动态代理在Java中有着广泛的应用,比如SpringAOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。
本文主要介绍Java中两种常见的动态代理方式:JDK原生动态代理和CGLIB动态代理。
由于Java动态代理与java反射机制关系紧密,请读者确保已经了解了Java反射机制,可参考上一篇文章《Java反射机制详解》
代理模式
本文将介绍的Java动态代理与设计模式中的代理模式有关,什么是代理模式呢?
代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
代理模式角色分为3种:
Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
RealSubject(真实主题角色):真正实现业务逻辑的类;
Proxy(代理主题角色):用来代理和封装真实主题;
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层
代理模式按照职责(使用场景)来分类,至少可以分为以下几类:1、远程代理。2、虚拟代理。3、Copy-on-Write代理。4、保护(ProtectorAccess)代理。5、Cache代理。6、防火墙(Firewall)代理。7、同步化(Synchronization)代理。8、智能引用(SmartReference)代理等等。
如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:
- 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
- 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
静态代理
我们先通过实例来学习静态代理,然后理解静态代理的缺点,再来学习本文的主角:动态代理
编写一个接口UserService,以及该接口的一个实现类UserServiceImpl
publicinterfaceUserService{
publicvoidselect();
publicvoidupdate();
}
publicclassUserServiceImplimplementsUserService{
publicvoidselect(){
System.out.println("查询selectById");
}
publicvoidupdate(){
System.out.println("更新update");
}
}
我们将通过静态代理对UserServiceImpl进行功能增强,在调用select
和update
之前记录一些日志。写一个代理类UserServiceProxy,代理类需要实现UserService
publicclassUserServiceProxyimplementsUserService{
privateUserServicetarget;//被代理的对象
publicUserServiceProxy(UserServicetarget){
this.target=target;
}
publicvoidselect(){
before();
target.select();//这里才实际调用真实主题角色的方法
after();
}
publicvoidupdate(){
before();
target.update();//这里才实际调用真实主题角色的方法
after();
}
privatevoidbefore(){//在执行方法之前执行
System.out.println(String.format("logstarttime[%s]",newDate()));
}
privatevoidafter(){//在执行方法之后执行
System.out.println(String.format("logendtime[%s]",newDate()));
}
}
客户端测试
publicclassClient1{
publicstaticvoidmain(String[]args){
UserServiceuserServiceImpl=newUserServiceImpl();
UserServiceproxy=newUserServiceProxy(userServiceImpl);
proxy.select();
proxy.update();
}
}
输出
logstarttime[ThuDec2014:13:25CST2018]
查询selectById
logendtime[ThuDec2014:13:25CST2018]
logstarttime[ThuDec2014:13:25CST2018]
更新update
logendtime[ThuDec2014:13:25CST2018]
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。
静态代理的缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
1、当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
如何改进?
当然是让代理类动态的生成啦,也就是动态代理。
为什么类可以动态的生成?
这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节类加载的过程。
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
- 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
- 从网络中获取,典型的应用是Applet
- 运行时计算生成,这种场景使用最多的是动态代理技术,在java.lang.reflect.Proxy类中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为
*$Proxy
的代理类的二进制字节流 - 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
- 从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。
常见的字节码操作类库
这里有一些介绍:https://java-source.net/open-source/bytecode-libraries
- ApacheBCEL(ByteCodeEngineeringLibrary):是Javaclassworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
- ObjectWebASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
- CGLIB(CodeGenerationLibrary):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
- Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库;它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
- ...
实现动态代理的思考方向
为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:
- 通过实现接口的方式->JDK动态代理
- 通过继承类的方式->CGLIB动态代理
注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦
JDK动态代理
JDK动态代理主要涉及两个类:java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
,我们仍然通过案例来学习
编写一个调用逻辑处理器LogHandler类,提供日志增强功能,并实现InvocationHandler接口;在LogHandler中维护一个目标对象,这个对象是被代理的对象(真实主题角色);在invoke
方法中编写方法调用的逻辑处理
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Method;
importjava.util.Date;
publicclassLogHandlerimplementsInvocationHandler{
Objecttarget;//被代理的对象,实际的方法执行者
publicLogHandler(Objecttarget){
this.target=target;
}
@Override
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
before();
Objectresult=method.invoke(target,args);//调用target的method方法
after();
returnresult;//返回方法的执行结果
}
//调用invoke方法之前执行
privatevoidbefore(){
System.out.println(String.format("logstarttime[%s]",newDate()));
}
//调用invoke方法之后执行
privatevoidafter(){
System.out.println(String.format("logendtime[%s]",newDate()));
}
}
编写客户端,获取动态生成的代理类的对象须借助Proxy类的newProxyInstance方法,具体步骤可见代码和注释
importproxy.UserService;
importproxy.UserServiceImpl;
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Proxy;
publicclassClient2{
publicstaticvoidmain(String[]args)throwsIllegalAccessException,InstantiationException{
//设置变量可以保存动态代理类,默认名称以$Proxy0格式命名
//System.getProperties().setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
//1.创建被代理的对象,UserService接口的实现类
UserServiceImpluserServiceImpl=newUserServiceImpl();
//2.获取对应的ClassLoader
ClassLoaderclassLoader=userServiceImpl.getClass().getClassLoader();
//3.获取所有接口的Class,这里的UserServiceImpl只实现了一个接口UserService,
Class[]interfaces=userServiceImpl.getClass().getInterfaces();
//4.创建一个将传给代理类的调用请求处理器,处理所有的代理对象上的方法调用
//这里创建的是一个自定义的日志处理器,须传入实际的执行对象userServiceImpl
InvocationHandlerlogHandler=newLogHandler(userServiceImpl);
/*
5.根据上面提供的信息,创建代理对象在这个过程中,
a.JDK会通过根据传入的参数信息动态地在内存中创建和.class文件等同的字节码
b.然后根据相应的字节码转换成对应的class,
c.然后调用newInstance()创建代理实例
*/
UserServiceproxy=(UserService)Proxy.newProxyInstance(classLoader,interfaces,logHandler);
//调用代理的方法
proxy.select();
proxy.update();
//保存JDK动态代理生成的代理类,类名保存为UserServiceProxy
//ProxyUtils.generateClassFile(userServiceImpl.getClass(),"UserServiceProxy");
}
}
运行结果
logstarttime[ThuDec2016:55:19CST2018]
查询selectById
logendtime[ThuDec2016:55:19CST2018]
logstarttime[ThuDec2016:55:19CST2018]
更新update
logendtime[ThuDec2016:55:19CST2018]
InvocationHandler和Proxy的主要方法介绍如下:
java.lang.reflect.InvocationHandler
Objectinvoke(Objectproxy,Methodmethod,Object[]args)
定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用
java.lang.reflect.Proxy
staticInvocationHandlergetInvocationHandler(Objectproxy)
用于获取指定代理对象所关联的调用处理器
staticClass<?>getProxyClass(ClassLoaderloader,Class<?>...interfaces)
返回指定接口的代理类
staticObjectnewProxyInstance(ClassLoaderloader,Class<?>[]interfaces,InvocationHandlerh)
构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的invoke方法
staticbooleanisProxyClass(Class<?>cl)
返回cl是否为一个代理类
代理类的调用过程
生成的代理类到底长什么样子呢?借助下面的工具类,把代理类保存下来再探个究竟
(通过设置环境变量sun.misc.ProxyGenerator.saveGeneratedFiles=true也可以保存代理类)
importsun.misc.ProxyGenerator;
importjava.io.FileOutputStream;
importjava.io.IOException;
publicclassProxyUtils{
/**
*将根据类信息动态生成的二进制字节码保存到硬盘中,默认的是clazz目录下
*params:clazz需要生成动态代理类的类
*proxyName:为动态生成的代理类的名称
*/
publicstaticvoidgenerateClassFile(Classclazz,StringproxyName){
//根据类信息和提供的代理类名称,生成字节码
byte[]classFile=ProxyGenerator.generateProxyClass(proxyName,clazz.getInterfaces());
Stringpaths=clazz.getResource(".").getPath();
System.out.println(paths);
FileOutputStreamout=null;
try{
//保留到硬盘中
out=newFileOutputStream(paths+proxyName+".class");
out.write(classFile);
out.flush();
}catch(Exceptione){
e.printStackTrace();
}finally{
try{
out.close();
}catch(IOExceptione){
e.printStackTrace();
}
}
}
}
然后在Client2测试类的main的最后面加入一行代码
//保存JDK动态代理生成的代理类,类名保存为UserServiceProxy
ProxyUtils.generateClassFile(userServiceImpl.getClass(),"UserServiceProxy");
IDEA再次运行之后就可以在target的类路径下找到UserServiceProxy.class,双击后IDEA的反编译插件会将该二进制class文件
UserServiceProxy的代码如下所示:
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Method;
importjava.lang.reflect.Proxy;
importjava.lang.reflect.UndeclaredThrowableException;
importproxy.UserService;
publicfinalclassUserServiceProxyextendsProxyimplementsUserService{
privatestaticMethodm1;
privatestaticMethodm2;
privatestaticMethodm4;
privatestaticMethodm0;
privatestaticMethodm3;
publicUserServiceProxy(InvocationHandlervar1)throws{
super(var1);
}
publicfinalbooleanequals(Objectvar1)throws{
//省略...
}
publicfinalStringtoString()throws{
//省略...
}
publicfinalvoidselect()throws{
try{
super.h.invoke(this,m4,(Object[])null);
}catch(RuntimeException|Errorvar2){
throwvar2;
}catch(Throwablevar3){
thrownewUndeclaredThrowableException(var3);
}
}
publicfinalinthashCode()throws{
//省略...
}
publicfinalvoidupdate()throws{
try{
super.h.invoke(this,m3,(Object[])null);
}catch(RuntimeException|Errorvar2){
throwvar2;
}catch(Throwablevar3){
thrownewUndeclaredThrowableException(var3);
}
}
static{
try{
m1=Class.forName("java.lang.Object").getMethod("equals",Class.forName("java.lang.Object"));
m2=Class.forName("java.lang.Object").getMethod("toString");
m4=Class.forName("proxy.UserService").getMethod("select");
m0=Class.forName("java.lang.Object").getMethod("hashCode");
m3=Class.forName("proxy.UserService").getMethod("update");
}catch(NoSuchMethodExceptionvar2){
thrownewNoSuchMethodError(var2.getMessage());
}catch(ClassNotFoundExceptionvar3){
thrownewNoClassDefFoundError(var3.getMessage());
}
}
}
从UserServiceProxy的代码中我们可以发现:
- UserServiceProxy继承了Proxy类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法
- 由于UserServiceProxy继承了Proxy类,所以每个代理类都会关联一个InvocationHandler方法调用处理器
- 类和所有方法都被
publicfinal
修饰,所以代理类只可被使用,不可以再被继承 - 每个方法都有一个Method对象来描述,Method对象在static静态代码块中创建,以
m+数字
的格式命名 - 调用方法的时候通过
super.h.invoke(this,m1,(Object[])null);
调用,其中的super.h.invoke
实际上是在创建代理的时候传递给Proxy.newProxyInstance
的LogHandler对象,它继承InvocationHandler类,负责实际的调用处理逻辑
而LogHandler的invoke方法接收到method、args等参数后,进行一些处理,然后通过反射让被代理的对象target执行方法
@Override
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{
before();
Objectresult=method.invoke(target,args);//调用target的method方法
after();
returnresult;//返回方法的执行结果
}
JDK动态代理执行方法调用的过程简图如下:
代理类的调用过程相信大家都明了了,而关于Proxy的源码解析,还请大家另外查阅其他文章或者直接看源码
CGLIB动态代理
maven引入CGLIB包,然后编写一个UserDao类,它没有接口,只有两个方法,select()和update()
publicclassUserDao{
publicvoidselect(){
System.out.println("UserDao查询selectById");
}
publicvoidupdate(){
System.out.println("UserDao更新update");
}
}
编写一个LogInterceptor,继承了MethodInterceptor,用于方法的拦截回调
importjava.lang.reflect.Method;
importjava.util.Date;
publicclassLogInterceptorimplementsMethodInterceptor{
/**
*@paramobject表示要进行增强的对象
*@parammethod表示拦截的方法
*@paramobjects数组表示参数列表,基本数据类型需要传入其包装类型,如int-->Integer、long-Long、double-->Double
*@parammethodProxy表示对方法的代理,invokeSuper方法表示对被代理对象方法的调用
*@return执行结果
*@throwsThrowable
*/
@Override
publicObjectintercept(Objectobject,Methodmethod,Object[]objects,MethodProxymethodProxy)throwsThrowable{
before();
Objectresult=methodProxy.invokeSuper(object,objects);//注意这里是调用invokeSuper而不是invoke,否则死循环,methodProxy.invokesuper执行的是原始类的方法,method.invoke执行的是子类的方法
after();
returnresult;
}
privatevoidbefore(){
System.out.println(String.format("logstarttime[%s]",newDate()));
}
privatevoidafter(){
System.out.println(String.format("logendtime[%s]",newDate()));
}
}
测试
importnet.sf.cglib.proxy.Enhancer;
publicclassCglibTest{
publicstaticvoidmain(String[]args){
DaoProxydaoProxy=newDaoProxy();
Enhancerenhancer=newEnhancer();
enhancer.setSuperclass(Dao.class);//设置超类,cglib是通过继承来实现的
enhancer.setCallback(daoProxy);
Daodao=(Dao)enhancer.create();//创建代理类
dao.update();
dao.select();
}
}
运行结果
logstarttime[FriDec2100:06:40CST2018]
UserDao查询selectById
logendtime[FriDec2100:06:40CST2018]
logstarttime[FriDec2100:06:40CST2018]
UserDao更新update
logendtime[FriDec2100:06:40CST2018]
还可以进一步多个MethodInterceptor进行过滤筛选
publicclassLogInterceptor2implementsMethodInterceptor{
@Override
publicObjectintercept(Objectobject,Methodmethod,Object[]objects,MethodProxymethodProxy)throwsThrowable{
before();
Objectresult=methodProxy.invokeSuper(object,objects);
after();
returnresult;
}
privatevoidbefore(){
System.out.println(String.format("log2starttime[%s]",newDate()));
}
privatevoidafter(){
System.out.println(String.format("log2endtime[%s]",newDate()));
}
}
//回调过滤器:在CGLib回调时可以设置对不同方法执行不同的回调逻辑,或者根本不执行回调。
publicclassDaoFilterimplementsCallbackFilter{
@Override
publicintaccept(Methodmethod){
if("select".equals(method.getName())){
return0;//Callback列表第1个拦截器
}
return1;//Callback列表第2个拦截器,return2则为第3个,以此类推
}
}
再次测试
publicclassCglibTest2{
publicstaticvoidmain(String[]args){
LogInterceptorlogInterceptor=newLogInterceptor();
LogInterceptor2logInterceptor2=newLogInterceptor2();
Enhancerenhancer=newEnhancer();
enhancer.setSuperclass(UserDao.class);//设置超类,cglib是通过继承来实现的
enhancer.setCallbacks(newCallback[]{logInterceptor,logInterceptor2,NoOp.INSTANCE});//设置多个拦截器,NoOp.INSTANCE是一个空拦截器,不做任何处理
enhancer.setCallbackFilter(newDaoFilter());
UserDaoproxy=(UserDao)enhancer.create();//创建代理类
proxy.select();
proxy.update();
}
}
运行结果
logstarttime[FriDec2100:22:39CST2018]
UserDao查询selectById
logendtime[FriDec2100:22:39CST2018]
log2starttime[FriDec2100:22:39CST2018]
UserDao更新update
log2endtime[FriDec2100:22:39CST2018]
CGLIB创建动态代理类的模式是:
- 查找目标类上的所有非final的public类型的方法定义;
- 将这些方法的定义转换成字节码;
- 将组成的字节码转换成相应的代理的class对象;
- 实现MethodInterceptor接口,用来处理对代理类上所有方法的请求
JDK动态代理与CGLIB动态代理对比
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。
cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDKProxy的优势:
- 最小化依赖关系,减少依赖意味着简化开发和维护,JDK本身的支持,可能比cglib更加可靠。
- 平滑进行JDK版本升级,而字节码类库通常需要进行更新以保证在新版Java上能够使用。
- 代码实现简单。
基于类似cglib框架的优势:
- 无需实现接口,达到代理类无侵入
- 只操作我们关心的类,而不必为其他相关类增加工作量。
- 高性能
面试题
来源于网上,用于帮助理解和掌握,欢迎补充
描述动态代理的几种实现方式?分别说出相应的优缺点
代理可以分为"静态代理"和"动态代理",动态代理又分为"JDK动态代理"和"CGLIB动态代理"实现。
静态代理:代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是RealObject
- 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
- 缺点:不同的接口要有不同的代理类实现,会很冗余
JDK动态代理:
- 为了解决静态代理中,生成大量的代理类造成的冗余;
- JDK动态代理只需要实现InvocationHandler接口,重写invoke方法便可以完成代理的实现,
- jdk的代理是利用反射生成代理类Proxyxx.class代理类字节码,并生成对象
- jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口
- 优点:解决了静态代理中冗余的代理实现类问题。
- 缺点:JDK动态代理是基于接口设计实现的,如果没有接口,会抛异常。
CGLIB代理:
- 由于JDK动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;
- CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。
- 实现方式实现MethodInterceptor接口,重写intercept方法,通过Enhancer类的回调方法来实现。
- 但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
- 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
- 优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。
- 缺点:技术实现相对难理解些。
CGlib对接口实现代理?
importnet.sf.cglib.proxy.Enhancer;
importnet.sf.cglib.proxy.MethodInterceptor;
importnet.sf.cglib.proxy.MethodProxy;
importproxy.UserService;
importjava.lang.reflect.Method;
/**
*创建代理类的工厂该类要实现MethodInterceptor接口。
*该类中完成三样工作:
*(1)声明目标类的成员变量,并创建以目标类对象为参数的构造器。用于接收目标对象
*(2)定义代理的生成方法,用于创建代理对象。方法名是任意的。代理对象即目标类的子类
*(3)定义回调接口方法。对目标类的增强这在这里完成
*/
publicclassCGLibFactoryimplementsMethodInterceptor{
//声明目标类的成员变量
privateUserServicetarget;
publicCGLibFactory(UserServicetarget){
this.target=target;
}
//定义代理的生成方法,用于创建代理对象
publicUserServicemyCGLibCreator(){
Enhancerenhancer=newEnhancer();
//为代理对象设置父类,即指定目标类
enhancer.setSuperclass(UserService.class);
/**
*设置回调接口对象注意,只所以在setCallback()方法中可以写上this,
*是因为MethodIntecepter接口继承自Callback,是其子接口
*/
enhancer.setCallback(this);
return(UserService)enhancer.create();//create用以生成CGLib代理对象
}
@Override
publicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{
System.out.println("startinvoke"+method.getName());
Objectresult=method.invoke(target,args);
System.out.println("endinvoke"+method.getName());
returnresult;
}
}
参考:
《Java核心技术》卷1
《深入理解Java虚拟机》7.3
javadocs:https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Proxy.html
Java三种代理模式:静态代理、动态代理和cglib代理
描述动态代理的几种实现方式分别说出相应的优缺点
JDK动态代理详解
Java动态代理机制详解(JDK和CGLIB,Javassist,ASM)
静态代理和动态代理的理解
后记
欢迎评论、转发、分享,您的支持是我最大的动力
更多内容可访问我的个人博客:http://laijianfeng.org
关注【小旋锋】微信公众号,及时接收博文推送
本文内容总结:代理模式,静态代理,JDK动态代理,CGLIB动态代理,JDK动态代理与CGLIB动态代理对比,面试题,后记,
原文链接:https://www.cnblogs.com/whirly/p/10154887.html