刚带完一个项目,组里新人问我:“Java代理模式不就是AOP那点事吗?看了一圈博客还是不太明白。”我让他把手头的代码写完,然后从头到尾讲了一遍静态代理、JDK动态代理、CGLIB和Spring AOP的关系。他听完说:“原来不是一个东西,是一整条线。”
这篇就按那次思路整理出来。先搞清楚代理到底在解决什么问题,再看三种经典实现方式分别怎么落地,最后聊聊框架里那些“看起来很高端”的注解,底层到底做了什么。如果你正准备面试,或者工作中经常和框架打交道,这篇可以帮你把代理这块彻底打通。
1. 代理模式到底在解决什么问题
1.1 从一段“加日志”的需求说起
假设有个订单服务,接口很简单:
public interface OrderService { void createOrder(String orderId); void cancelOrder(String orderId); }实现类也很直接:
public class OrderServiceImpl implements OrderService { @Override public void createOrder(String orderId) { System.out.println("创建订单:" + orderId); } @Override public void cancelOrder(String orderId) { System.out.println("取消订单:" + orderId); } }运行没几天,老板说:“所有订单操作都给我加上日志,记录谁在什么时间做了什么。”你直接在实现类里加日志,是能完成任务,但问题随之而来:
- 日志逻辑散落在所有业务方法里,改个日志格式要翻遍全部代码;
- 每次新加业务方法,都要记得补一段日志代码,漏一个就出事故;
- 如果还想加权限校验、性能统计、事务控制,每个方法都要继续往里塞,代码变得越来越臃肿。
代理模式解决的核心问题就是:把通用逻辑从业务代码中剥离出来,在不修改原类代码的前提下,增强对象的行为。
这和装修房子很像。你请的装修队(代理)不会改变房屋结构(原类),但会在你入住前把水电、防水、墙面这些“非房子主体但不可或缺”的事情做好,你拿到的还是那套房,但住着更安全省心。业务代码就是那套房,日志、权限、事务这些就是装修队干的活。
1.2 代理模式的结构与角色划分
代理模式的类图很简单,就三个角色,但搞懂这三个角色的职责边界,是理解整个模式的关键:
| 角色 | 职责 | 类比 |
|---|---|---|
| 抽象主题(Subject) | 定义真实对象和代理对象的公共接口,调用方只面向接口编程 | 装修合同,约定交付标准 |
| 真实主题(RealSubject) | 真正执行业务逻辑的对象 | 房屋本身 |
| 代理(Proxy) | 持有真实主题引用,在调用前后插入额外逻辑 | 装修队 |
注意一个关键点:调用方操作的是代理对象,完全不感知真实对象的存在。这就带来两个好处:
- 调用方代码不需要做任何修改,想要增强就替换代理类即可;
- 代理类可以控制真实对象的创建时机和访问权限,比如延迟加载、访问控制。
我在早期写业务代码时,常犯的一个错是把代理和装饰器混为一谈。两者的意图有本质区别:装饰器模式侧重“增强功能”,比如给咖啡加奶加糖,结果还是咖啡;代理模式侧重“控制访问”,比如经纪人替明星接活,明星不直接对外暴露。动态代理里你可以在代理中做各种事情,但本质上它还是在替“真实对象”把门。
2. 三种经典实现方式对比
2.1 静态代理:最直观,也最“笨”
静态代理是指在编译期就确定代理类和真实类的对应关系,一个代理类对应一个接口。写法很简单:
public class OrderServiceProxy implements OrderService { private final OrderService target; private final UserService userService; public OrderServiceProxy(OrderService target, UserService userService) { this.target = target; this.userService = userService; } @Override public void createOrder(String orderId) { String operator = userService.getCurrentUser(); long start = System.currentTimeMillis(); try { target.createOrder(orderId); } finally { System.out.println("操作人:" + operator + ",耗时:" + (System.currentTimeMillis() - start) + "ms"); } } @Override public void cancelOrder(String orderId) { // 同样的增强逻辑 String operator = userService.getCurrentUser(); long start = System.currentTimeMillis(); try { target.cancelOrder(orderId); } finally { System.out.println("操作人:" + operator + ",耗时:" + (System.currentTimeMillis() - start) + "ms"); } } }这个版本有几个明显问题。
第一,代码重复。日志、耗时、权限校验这些逻辑在createOrder和cancelOrder里几乎一模一样,只是调用的目标方法名不同。接口每增加一个方法,代理类就要跟着增加一个方法,并复制粘贴一遍增强逻辑。
第二,维护成本高。业务接口有十几个方法,代理类就有一大坨模板代码。如果日志格式要调整,所有代理方法都要改动。更别提系统里有几十个业务类,每类配一个代理,类数量直接翻倍,项目里会堆出大量几乎相同结构的类,看得人头皮发麻。
第三,代理类和业务类强耦合。代理类里写死了OrderService接口,换一个业务模块就得重新写一个几乎长得一模一样的代理类,纯粹体力活。
静态代理也不是一无是处。当被代理的类很少、接口很稳定、增强逻辑相对固定时,静态代理写起来最快,也最容易阅读和调试,字节码层面完全透明,没有动态生成的类,排查问题没有任何神秘感。我在维护一些老项目时,看到过用静态代理做权限校验的写法,虽然笨,但效果直接,新接手的人一眼就能看懂。所以别一味追求“高级”的动态代理,静态代理在简单场景下反而是最简单可靠的方案。
2.2 JDK动态代理:运行时现造一个代理类
JDK动态代理的核心思路是:不预先手写代理类,而是在运行时动态生成一个实现了指定接口的代理类,并对方法调用进行统一拦截。
实现上只需要两步。第一步,让被代理的类实现一个接口;第二步,创建InvocationHandler,把增强逻辑写在invoke方法里:
public class LogHandler implements InvocationHandler { private final Object target; private final UserService userService; public LogHandler(Object target, UserService userService) { this.target = target; this.userService = userService; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String operator = userService.getCurrentUser(); long start = System.currentTimeMillis(); try { return method.invoke(target, args); } finally { System.out.println("方法:" + method.getName() + ",操作人:" + operator + ",耗时:" + (System.currentTimeMillis() - start) + "ms"); } } }第三步,通过Proxy.newProxyInstance创建代理对象:
OrderService target = new OrderServiceImpl(); OrderService proxy = (OrderService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogHandler(target, userService) ); proxy.createOrder("A1001");运行后你会发现,日志自动打印了,而且OrderService接口的每个方法都自动被增强,新增接口方法不需要改代理代码。
注意方法调用是method.invoke(target, args),而不是直接target.createOrder(args)。因为invoke方法里拿到的是Method对象,只能用反射方式调用真实方法。这也是初学者最容易卡住的地方:为什么我有目标对象不用,非要绕一圈反射?因为代理类是动态生成的,它不知道真实对象有哪些方法,只有通过Method对象这个“方法说明书”才能找到并调用正确的方法。
JDK动态代理有两个硬性条件:
- 被代理的类必须实现接口。代理类是JVM在运行时生成的,它继承自
Proxy类并实现了你传入的接口。Java是单继承,代理类已经继承了Proxy,所以只能靠实现接口来“冒充”你的业务类型。 - 只有接口里的方法能被拦截。如果你的实现类里有接口外独有的方法,代理对象是调用不到的,因为代理类的类型是接口类型,不是实现类类型。
2.3 CGLIB动态代理:没有接口也能代理
现实项目里,经常遇到没有接口的业务类,或者被代理的是第三方库的类,不能改源码。这时候JDK动态代理就失效了,需要CGLIB上场。
CGLIB的底层原理是:生成一个目标类的子类,在子类中重写父类的方法,并通过MethodInterceptor拦截方法调用。因为是基于继承的,所以严格来说CGLIB不是“代理模式”教科书意义上的代理,而是一种“子类增强”方案,但在实际框架中,它的职责和动态代理完全一致,大家也就习惯把它归入代理模式的动态实现行列。
public class CglibLogInterceptor implements MethodInterceptor { private final Object target; private final UserService userService; public CglibLogInterceptor(Object target, UserService userService) { this.target = target; this.userService = userService; } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { String operator = userService.getCurrentUser(); long start = System.currentTimeMillis(); try { return proxy.invokeSuper(obj, args); } finally { System.out.println("方法:" + method.getName() + ",操作人:" + operator + ",耗时:" + (System.currentTimeMillis() - start) + "ms"); } } }创建方式也略有不同:
OrderServiceImpl target = new OrderServiceImpl(); Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OrderServiceImpl.class); enhancer.setCallback(new CglibLogInterceptor(target, userService)); OrderServiceImpl proxy = (OrderServiceImpl) enhancer.create(); proxy.createOrder("A1001");注意到一个细节:intercept方法里用的是proxy.invokeSuper(obj, args),而不是method.invoke(obj, args)。这两者有什么区别?
method.invoke(obj, args)是通过反射调目标类的原始方法,性能相对较差;而MethodProxy.invokeSuper走的是CGLIB生成的快速通道,直接调用父类中被重写的方法,性能好得多。CGLIB在底层为每个方法生成了代理方法的索引,调用时通过索引直达目标,避开了反射的开销。这也是为什么在高频调用场景下CGLIB往往比JDK代理更快的原因之一。
但CGLIB也有自己的限制:
- final类无法被代理。CGLIB要生成子类,
final类不能被继承,直接报错。 - final方法无法被增强。子类无法重写
final方法,调用仍走原方法。 - private方法无法被增强。子类看不到父类的私有方法。
2.4 三种方式对比速查
| 对比维度 | 静态代理 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|---|
| 代理类生成时机 | 编译期 | 运行时 | 运行时 |
| 是否需要接口 | 需要 | 必须 | 不需要 |
| 底层实现 | 手写类 | 反射 + 动态生成接口实现类 | ASM字节码 + 生成子类 |
| 性能(调用开销) | 最优 | 反射有一定开销 | 比JDK更高一些,但比纯反射快 |
| 代码维护成本 | 高 | 低 | 低 |
| 限制 | 类数量膨胀 | 方法必须来自接口 | final类/方法不能代理 |
这里要特别注意,日常工作中动态代理的性能差异在绝大多数业务场景下并不值得焦虑。一次数据库查询、一次RPC调用动辄几十毫秒,代理反射多出来的那几微秒完全可以忽略。真正要关心的是代理的正确性,比如代理没生效、内部调用绕过代理这类问题。
3. 深入剖析动态代理的核心机制
3.1 JDK动态代理的代理类到底长什么样
刚才提到JDK动态代理会在运行时生成代理类。很多人好奇:这个类到底长什么样?其实可以用一个小技巧把它打印出来看源码。
在Proxy.newProxyInstance创建代理对象后,通过Proxy.getProxyClass获取Class对象,再通过反射拿字节码,反编译后你会发现生成的代理类结构大致如下:
public final class $Proxy0 extends Proxy implements OrderService { private static Method m1; // hashCode private static Method m2; // equals private static Method m3; // toString private static Method m4; // createOrder private static Method m5; // cancelOrder public $Proxy0(InvocationHandler h) { super(h); } public final void createOrder(String orderId) { try { super.h.invoke(this, m4, new Object[]{orderId}); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } // 其他方法类似 }看到这段伪代码,你就能理解为什么每次调用代理方法都会进入InvocationHandler.invoke。
几个关键点:
- 代理类持有
InvocationHandler引用,所有方法调用都转交给它的invoke方法; Method对象提前在静态代码块里拿到,避免每次调用都重新查找方法,这也是性能优化的一步;- 代理类自己并不实现任何业务逻辑,它只是一个“转发器”;
Object的三个方法(hashCode、equals、toString)也会被代理,这意味着你可以通过重写InvocationHandler来改变这些方法的行为。有个老梗说“动态代理可以让你连equals都不可信”,原理就在这里。
3.2 InvocationHandler和Method参数的关系
InvocationHandler的invoke方法有三个参数,很多初学者搞不清proxy和target的区别。
proxy:动态生成的代理对象本身。注意它的类型是接口类型,你如果在invoke强制把它转型为实现类,会抛ClassCastException。method:被调用的方法。它属于接口,不属于实现类。如果实现类覆写了方法但没在接口里声明,这个Method对象是不包含该方法的。args:方法调用参数。
一个常见的坑是:在invoke里调用method.invoke(proxy, args),而不是method.invoke(target, args)。如果这样写,会陷入无限递归,最终导致StackOverflowError。因为代理对象的方法调用会被InvocationHandler.invoke接管,你在invoke里又去调用代理对象的方法,就形成了“调用-转发-再调用”的死循环。
提示:如果目标类重写了
toString或hashCode,在invoke里要格外小心。你想着打印日志方便,结果触发了代理类的toString,又把控制权交回了invoke方法,轻则日志混乱,重则递归栈溢出。稳妥的做法是先判断method.getDeclaringClass() == Object.class,再决定是否放行。
3.3 CGLIB的字节码增强原理
CGLIB底层用的ASM库,直接操作字节码。生成子类后,CGLIB会为每个非final、非private的方法建立索引,并把MethodInterceptor挂在子类上。当调用子类对象的方法时,会进入MethodInterceptor.intercept,该方法内部通过MethodProxy触发对父类方法的调用。
MethodProxy有两个常用方法:
invokeSuper(obj, args):调用父类被重写的方法,性能好。invoke(obj, args):调用被代理类的原始方法。但在某些场景下会引入额外的问题,比如在拦截器里再次触发方法调用时可能引起栈溢出。
CGLIB还做了FastClass机制。普通反射调用是“根据方法名找方法,再做参数匹配”,FastClass则是为类和每个方法建立一个索引表,调用时直接按索引定位,少了反射查找的开销。这就是CGLIB性能接近“原生调用”的原因。
不过FastClass也有代价:生成额外的类,占用更多的元空间;首次创建时要计算所有方法的索引,初始化时间比JDK动态代理略长。在启动耗时敏感的场景,比如一些微服务启动优化时,这也是一个要考量的点。
3.4 动静态代理如何选型
选型没有一个放之四海皆准的答案,但有一条很实用的经验法则:
- 如果业务类有接口,优先用JDK动态代理。它实现简单、兼容性好、不依赖额外库,生成的类更干净,排查问题也更容易。
- 如果业务类没有接口,或者需要代理的是第三方类,CGLIB是必经之路。
- Spring在5.x之前默认使用JDK动态代理,除非
targetClass被明确指定为CGLIB。我碰到过一个老项目,接口方法代理生效但实现类内部方法未被拦截,查了半天才反应过来是JDK动态代理的路数。后面升级到Spring Boot 2.x时代,Spring AOP的默认策略变成了CGLIB优先,这类“神秘现象”就少了很多。 - 如果是研究学习,静态代理和JDK动态代理必跑一遍,CGLIB可以随手写个小Demo加深理解。
说到这里,其实有一个很微妙的地方:CGLIB的“默认优先”也带来过新坑,比如对没有默认构造函数的类、对final方法的处理、对CGLIB代理对象做JSON序列化时出现循环引用。所以选型要结合具体场景,不是简单选“哪个更快”就完事。
4. 代理模式在框架中的真实应用
4.1 Spring AOP的实现选型
Spring AOP是代理模式的集大成者。它在运行时动态判断:
- 如果目标对象实现了接口,默认走JDK动态代理;
- 如果没有实现接口,或显式开启了
proxyTargetClass,走CGLIB子类代理。
Spring把这两类代理封装在统一的结构里,对外暴露的是AopProxy接口。你写切面时根本不需要关心底层用了哪种代理,只要关注切点表达式和通知类型。但这种“便利”也有代价,一旦代理没生效,排查起捉襟见肘。最常见的坑之一就是类内部方法调用不走代理。
举个例子:
@Service public class OrderServiceImpl { @Transactional public void createOrder(String orderId) { // 事务逻辑 } public void createOrderWithLog(String orderId) { createOrder(orderId); // 内部直接调用 } }createOrderWithLog内部调用createOrder时,走的是this.createOrder,也就是当前对象的原生方法,而不是Spring的代理对象,所以@Transactional不会在内部方法调用时生效。这个“自调用问题”几乎每个月都能碰到有人踩坑。解决办法有三种:
- 在内部通过
ApplicationContext.getBean拿到代理对象再调用; - 把逻辑拆到另一个Service里,让外部调用走代理;
- 用
AopContext.currentProxy(),但注意需要在配置里开启exposeProxy。
4.2 MyBatis的Mapper其实也是代理
你执行userMapper.selectById(1)时,UserMapper只是一个接口,没有实现类。MyBatis在启动时通过MapperProxy动态生成代理对象,把接口方法解析成SQL,再交给SqlSession执行。
MapperProxy实现了InvocationHandler,核心逻辑在invoke里判断:如果是Object的方法(如toString)就放行,否则找到对应的MappedStatement执行SQL。
这里有一个很酷的细节:正是因为动态代理,才能实现“接口即DAO”这种极简写法。你没有写实现类,但运行时所有方法调用都被代理接管,MyBatis只需要在代理里做“方法签名到SQL语句”的映射。理解了这一点,看MyBatis源码时就豁然开朗。
4.3 延迟加载与远程调用的代理思路
除了AOP和数据库访问,动态代理在一些中间件里也扮演关键角色。
比如RPC框架(像一些内部服务框架)的@Reference注解,实际上就是生成了一个远程代理对象。本地调用这个代理的方法,代理内部通过网络把请求发给远程服务器,拿到结果再返回。对于调用方来说,就像在调用一个本地方法。
再比如ORM框架里的懒加载代理。你拿到一个订单对象,里面有用户信息字段,但是直到你真的调用getUser()时才去加载数据库。这个“咦,明明没有加载但调用时却自动加载了”的效果,就是通过代理把getUser方法拦截下来,先查数据库再返回结果。
延迟加载代理还是代理模式“控制访问”这一本质的最佳体现,它可以精细控制什么时候执行真正的加载动作,甚至可以在加载失败时给出兜底逻辑。
5. 常见问题与排查技巧实录
5.1 代理对象类型转换失败
现象:
OrderService proxy = Proxy.newProxyInstance(...); OrderServiceImpl impl = (OrderServiceImpl) proxy; // ClassCastException原因:JDK动态代理生成的$Proxy0是OrderService接口的实现类,但不是OrderServiceImpl的子类,两者没有任何继承关系,强制转换当然失败。这其实是很多人的认知盲区:代理对象不等于目标对象。
解决:始终面向接口使用代理对象。
OrderService proxy = (OrderService) Proxy.newProxyInstance(...);如果实在要用实现类的类型,就得用CGLIB,因为它生成的子类天然兼容实现类类型。
5.2 Spring事务不起作用
现象:方法上加了@Transactional但数据库数据没回滚,或者某些情况根本没有开启事务。
排查思路:
- 确认Spring AOP是否生效。代理对象是否真的被注入了?检查是否通过
this.xxx()内部调用,绕过代理; - 确认
@Transactional所在的方法是否为public。Spring默认用JDK代理或CGLIB,但通知逻辑只增强public方法,非public方法不走事务代理; - 确认事务方法是否在“初始化之后”才调用。如果在构造器或字段初始化时调用,代理还没建立,肯定不生效。
5.3 CGLIB代理对象序列化的坑
现象:把代理对象存Redis或转JSON时,出现循环引用或报错。
原因:CGLIB代理对象内部持有目标对象引用,目标对象可能又反向引用代理对象,序列化时就形成了循环依赖。
解决:序列化前先获取目标对象,或使用FastJSON等库配置SerializerFeature.DisableCircularReferenceDetect不仅仅能缓解,还要注意尽量让DTO不包含代理引用。
5.4 显式指定CGLIB有时会报错
现象:Spring配置proxyTargetClass=true后,某些类创建代理失败。
原因:目标的构造函数不是可访问的,或目标类有无法被Callable机制正确处理的情况。实际上Spring 6.x里已经移除了对CGLIB的默认启用方式,转而要求你显式引入CGLIB支持或改用Lite模式。这里不需要死记细节,但要记得:动态代理依赖字节码库的版本和JVM版本兼容性,升级JDK版本时,如果引用了较老的CGLIB或ASM库,可能出现IllegalAccessError或NoClassDefFoundError,升级中间件时最好同步关注字节码库版本。
5.5 代理叠加导致逻辑混乱
现象:某个方法被加了事务、日志、权限、缓存好几个切面,执行顺序和预期不一致。
原因:多个代理会按@Order排序层层包裹。事务、安全、缓存等切面各司其职,调用顺序由切面优先级决定。
解决:不要试图在切面里做太多事。一个切面只做一类增强,优先级通过Order控制。如果需要更复杂的组合逻辑,放到切面内部再聚合。代理叠加本身是框架设计中的重要一环,搞混优先级就会出现业务行为与预期相悖的情况。
5.6 用“视觉验证”确认代理是否生效
排查代理问题时,一个最直接的方法就是打断点看对象类型。假如你注入了OrderService,发现类型是$Proxy0或OrderServiceImpl$$EnhancerBySpringCGLIB,说明代理生效;如果是原生OrderServiceImpl,说明代理没有创建成功。
另外可以在配置文件里看启动日志:
- Spring Boot自动配置开启AOP时,会打印类似“代理方式”的日志;
- 偏门做法是通过
AopUtils.isAopProxy(obj)判断是否为代理对象,也能直观确认。
提示:写代码时多留意“你操作的是不是代理对象”这个潜意识。很多迷之Bug,本质上都是没有意识到某些对象已经被代理替换了。
6. 一个最小可用示例:自己动手实现一个“事务增强”
理论的尽头是代码。我建议你写一个可控的最小示例来彻底理解动态代理的用法。以下是一个模拟版本的事务增强,不依赖Spring,纯粹用JDK代理实现“开启事务-执行-提交/回滚”的骨架:
public class TransactionHandler implements InvocationHandler { private final Object target; public TransactionHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { boolean isTransactional = method.isAnnotationPresent(MyTransactional.class); if (!isTransactional) { return method.invoke(target, args); } System.out.println("开启事务"); try { Object result = method.invoke(target, args); System.out.println("提交事务"); return result; } catch (Exception e) { System.out.println("回滚事务"); throw e; } } }配合一个自定义注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyTransactional { }测试入口:
public class TransactionTest { public static void main(String[] args) { UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new TransactionHandler(target) ); proxy.createUser("zhangsan"); } }运行后观察打印顺序:无注解方法直接执行,有注解方法会打印“开启事务-执行方法-提交事务”或“开启事务-执行方法-回滚事务”。
这个练习不是为了让你去实现事务框架,而是帮你建立“代理本质是拦截方法调用”的心智模型。一旦掌握了这个模型,理解AOP会非常快,因为Spring AOP的@Around通知就是在这个模型上加了切点匹配和通知链编排而已。
7. 实战心得与避坑清单
最后分享几条实用的心得,都是换过血的教训。
第一,动态代理不是万能的。遇到性能瓶颈时,先查看是不是代理层做了大量不必要的工作。我见过有人在InvocationHandler.invoke里做MDC日志、权限拦截、耗时统计、参数校验、黑名单过滤,“全家桶”式堆在一个代理里,结果调用链一长,性能直落。一个代理只做一件事,多件事就拆多个代理,或者用责任链模式编排。
第二,内部方法调用是最大的“隐形杀手”。无论Spring还是MyBatis,代理生效的前提是“从外部进入代理对象”。一旦内部this调用,代理就失效。设计接口时尽量保持方法粒度合理,避免在一个业务方法里调用同类的其他事务方法。如果无法避免,用AopContext.currentProxy()显式获取代理对象。
第三,学习动态代理一定要把手“弄脏”。打开IDE写一遍规范的示例,选一个中间件,比如MyBatis的MapperProxy,或者自己模仿写一个RPC框架的最小代理,再回来看Spring AOP,你会觉得那些神秘概念其实就是这几行代码的排列组合。
第四,代理模式与AOP的边界要想清楚。代理是“结构型模式”里的经典模式,AOP是“利用代理机制实现横切关注点”的编程范式。代理提供了能力,AOP定义了使用方式。很多人面试被问“AOP原理”,答不上来就是没把代理模式吃透。反过来,动态代理又是AOP落地的核心基础设施。两者互为表里,没必要割裂着记忆。
最后再分享一个小技巧:在InvocationHandler里统一处理Object方法时,尽量保持默认行为。我在项目中见过有人为了让日志方便,在toString上夹带私货,结果导致基于字符串拼接的日志输出混乱,排查了好久才发现是代理的toString被重写了。默认行为就是放行,别给Object方法添加多余逻辑,除非你完全清楚自己在干什么。
代理模式的代码量并不大,但它对理解框架源码、排查线上疑难问题、设计可扩展架构都至关重要。把静态代理、JDK动态代理、CGLIB走一遍,再去看Spring AOP,你会发现那些以前觉得“高深莫测”的框架机制,并没有你想的那么遥远。