做Java开发这些年有两个面试题在我自己招人的时候几乎必问一个是HashMap在JDK 1.8里到底怎么扩容另一个就是JDK动态代理的底层原理。前者考的是数据结构的工程取舍后者考的是对Java语言机制的理解深度。很多人把Spring AOP用得很熟注解一加切面就生效但真要问一句“Spring到底是怎么帮你在目标方法前后插进来一段逻辑的”往往就卡住了。JDK动态代理就是解开这个疑惑的第一把钥匙。这篇文章会把JDK动态代理从“怎么用”到“为什么能这么用”完整拆开讲清楚。我会先从一个最简单的示例入手让你看到代理是怎么跑起来的然后再深入到Proxy类源码看看那些动态生成的代理类到底长什么样子、由谁生成、怎么生成的。最后再花篇幅说说JDK动态代理和CGLIB的区别、在Spring AOP里的实际选择逻辑以及我踩过的几个坑。不管你是准备面试还是想在项目里自己封装日志、权限、事务这类横切逻辑这篇文章的内容都够用。1. 项目概述与场景定位1.1 动态代理到底解决了什么问题开始讲原理之前先想清楚一个问题我们已经有了继承和组合为什么还需要代理举个例子你有一个UserService接口里面定义了saveUser、getUserById等方法。现在产品提了个需求所有方法调用都要记录日志包括调用参数、返回结果、耗时。第一反应是在每个方法里加日志代码但这意味着所有实现类都要改一遍而且日志逻辑会和业务逻辑混在一起后面想单独调整日志策略还得再动业务代码。代理机制做的事情就是——你调用UserService的时候并不直接调真实实现类而是先经过一个“中间人”。中间人可以在调真实方法之前和之后插入额外操作比如打印日志、做权限校验、开启事务。这个中间人就是代理对象。如果这个代理对象是在程序运行期动态创建出来的不需要你手动写一个类那它就叫做动态代理。这个模式解决的核心痛点就是把横切逻辑和业务逻辑解耦。所谓横切逻辑就是那些跟业务无关、但是每个方法可能都要做的事日志、事务、权限、限流、缓存。动态代理让你把这些逻辑集中在一个地方维护业务类保持干净专注于自己的事情。1.2 JDK动态代理在整个代理体系中的位置代理模式是设计模式里非常基础的一种但在Java生态里具体落地方式有好几种。静态代理需要你手写代理类一个接口一个实现类维护成本高基本没人这么干了。剩下的主流方案就是JDK动态代理和CGLIB两者的核心差异一句话就能说清JDK动态代理只能代理接口CGLIB可以通过生成子类的方式代理类。JDK动态代理是Java原生支持的能力从JDK 1.3就存在了核心类就两个java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。它的最大优势是不需要引入任何第三方依赖JVM自己就能生成代理类缺点是它只能针对接口干活如果你的目标对象压根没有实现任何接口它就没辙了。CGLIB则是通过继承目标类来创建代理不要求目标实现接口但也有自己的局限比如final类和方法无法被代理。上面这个背景理清了后面讲原理和示例的时候你就知道每个细节是冲着什么问题去的。2. 核心原理拆解JDK动态代理的底层机制2.1 从Proxy.newProxyInstance方法说起先记住使用JDK动态代理的一套固定动作三步实现InvocationHandler接口、调用Proxy.newProxyInstance生成代理对象、通过代理对象调用方法。下面这段代码是骨架public interface UserService { void saveUser(String name); String getUserById(Long id); } public class UserServiceImpl implements UserService { Override public void saveUser(String name) { System.out.println(模拟保存用户: name); } Override public String getUserById(Long id) { return 用户 id; } }然后是这个关键方法public class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); System.out.println(调用方法: method.getName() , 参数: Arrays.toString(args)); Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println(方法执行耗时: cost ms); return result; } }生成代理对象的入口UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); proxy.saveUser(张三); String user proxy.getUserById(1L); System.out.println(user);跑起来之后你会看到日志先打印“调用方法: saveUser”再打印“模拟保存用户: 张三”最后打印耗时。也就是说你代码里明明调的是proxy.saveUser但实际上真正的执行顺序经过了InvocationHandler的invoke方法由invoke方法再去反射调用target的真实方法。这里要注意一个容易误导新手的点invoke方法的第一个参数proxy指代的是那个动态生成的代理对象本身不是你的目标对象target。如果你在invoke方法内部直接调用proxy上的方法比如通过method.invoke(proxy, args)就会造成无限递归栈溢出。所有真正的方法调用都应该通过method.invoke(target, args)来转发到真实对象上。2.2 InvocationHandler代理逻辑的真正入口InvocationHandler是一个函数式接口里面只有一个invoke方法。所有代理对象上的方法调用都会被JVM转发到这个invoke方法上。这里有一个很重要的问题为什么是转发到invoke而不是代理对象自己实现方法原因是代理对象是JVM在运行期动态生成的一个类它没法提前知道你要在方法前后做什么所以Java的设计者把“调用处理”这个动作抽象成了一个Handler。代理类只负责一件事接收到方法调用然后原封不动地交给InvocationHandler由你的代码来决定在调用前后做什么、要不要调用真实方法、甚至干脆不调用真实方法。理解了这一点你就能明白InvocationHandler的灵活之处。比如说你要做权限控制完全可以在invoke里面先判断当前用户是否有权限如果没权限直接抛异常或者返回null方法体根本不会执行。要做事务就是调用method.invoke之前开启事务调用之后根据是否抛异常决定提交还是回滚。我把这个机制的几个核心参数整理一下。参数类型含义典型使用场景proxyObject动态生成的代理实例主要用于toString、hashCode等方法的特殊处理不建议业务使用methodMethod被调用方法的反射对象判断方法名、获取注解、执行目标方法argsObject[]被调用方法的实参记录参数、修改参数、判断参数合法性2.3 动态生成的代理类长什么样很多同学学了JDK动态代理之后总有一个疑问代理对象到底是什么类型的它实现的是UserService接口但它不是UserServiceImpl。为了真正弄明白可以在代码里加一句System.out.println(proxy.getClass().getName());输出结果会是类似这样的com.sun.proxy.$Proxy0这个$Proxy0就是JVM在运行期自动生成的一个类。它有以下特点位于com.sun.proxy包下不同JDK版本包名可能有差异但$Proxy格式是一致的。继承自java.lang.reflect.Proxy。实现了你在newProxyInstance里传入的全部接口。类名编号默认从0开始$Proxy0是第一个生成的代理类后面依次递增。为什么$Proxy0要继承Proxy类因为代理对象归属的类加载器和InvocationHandler都是通过Proxy类的字段保存的。$Proxy0在构造时调用super(InvocationHandler)把Handler存到父类的h字段中。之后$Proxy0实现接口里的每个方法方法体内部就做一件事调用h.invoke(this, method, args)。这个method是常量池里静态定义的Method对象每个代理方法对应一个。这样一来整个调用链就通了你调用proxy.saveUser()实际执行的是$Proxy0.saveUser()方法体里面调用了super.h.invoke(this, saveUserMethod, args)然后就进入了你在InvocationHandler里编写的代码。3. 完整示例从零实现一个带日志增强的业务代理3.1 定义业务接口与目标类原理讲完了我们来做一个更完整、更接近实际开发场景的示例。假设你有一个电商项目需要一个OrderService接口提供创建订单和查询订单的方法。为了演示权限校验和日志记录两种增强逻辑我会把这两个横切关注点都放到InvocationHandler里。public interface OrderService { Long createOrder(String productId, int quantity); Order getOrderById(Long orderId); }对应的实现类业务逻辑尽量简单重点在演示代理public class OrderServiceImpl implements OrderService { Override public Long createOrder(String productId, int quantity) { System.out.println(【业务逻辑】创建订单: 商品 productId , 数量 quantity); return System.currentTimeMillis(); } Override public Order getOrderById(Long orderId) { System.out.println(【业务逻辑】查询订单: id orderId); return new Order(orderId, 默认商品, 1); } }Order是一个简单的POJO包含订单ID、商品名称、数量构造方法赋初值即可。为了让示例更完整我在Order上加了toString方法打印的时候能看清内容。3.2 实现带权限校验的InvocationHandler这个Handler做的事情比前面的日志版要复杂一些。我会在里面同时处理三件事方法级权限校验、日志记录、异常兜底。权限校验通过一个简单的模拟来判断——当前请求如果携带了admin角色才允许调用createOrder方法。public class OrderInvocationHandler implements InvocationHandler { private final Object target; private final String currentRole; public OrderInvocationHandler(Object target, String currentRole) { this.target target; this.currentRole currentRole; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.currentTimeMillis(); System.out.println( 代理增强开始 ); // 模拟权限校验: createOrder方法要求admin角色 if (method.getName().equals(createOrder) !admin.equals(currentRole)) { throw new SecurityException(无权限创建订单当前角色: currentRole); } Object result null; try { result method.invoke(target, args); return result; } catch (InvocationTargetException e) { Throwable cause e.getCause(); System.out.println(捕获异常: cause.getMessage()); throw cause; } finally { long cost System.currentTimeMillis() - start; System.out.println( 代理增强结束, 耗时: cost ms ); } } }这里有几个值得注意的细节。第一method.invoke(target, args)在目标方法内部抛出异常时反射会把它包装成InvocationTargetException所以要想拿到原始异常必须getCause()再重新抛出保证异常类型和业务方法定义的一致。第二如果目标方法返回基本类型int而你return null代理调用就会NPE所以不能随意在invoke里返回null需要考虑返回值类型。第三finally里可以做一些清理工作比如关闭资源、记录耗时即使方法抛异常也会执行。3.3 编写测试代码验证效果测试类里我分别用admin角色和普通user角色去调用代理验证权限控制的生效情况public class ProxyDemo { public static void main(String[] args) { OrderService target new OrderServiceImpl(); // admin角色调用 OrderService adminProxy (OrderService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new OrderInvocationHandler(target, admin) ); Long orderId adminProxy.createOrder(P1001, 2); System.out.println(创建订单成功: orderId); System.out.println(adminProxy.getOrderById(orderId)); System.out.println(--------------); // user角色调用 OrderService userProxy (OrderService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new OrderInvocationHandler(target, user) ); try { userProxy.createOrder(P1001, 2); } catch (SecurityException e) { System.out.println(权限校验生效: e.getMessage()); } } }运行结果大致如下 代理增强开始 【业务逻辑】创建订单: 商品P1001, 数量2 代理增强结束, 耗时: 1ms 创建订单成功: 1732000000000 代理增强开始 【业务逻辑】查询订单: id1732000000000 代理增强结束, 耗时: 0ms 订单{orderId1732000000000, productName默认商品, quantity1} -------------- 代理增强开始 捕获异常: 无权限创建订单当前角色: user 权限校验生效: 无权限创建订单当前角色: user可以看到同样的接口、同样的目标类在不同InvocationHandler里传了不同的角色参数行为就完全不一样。这就是代理的魅力你不需要改动OrderServiceImpl一行代码就完成了权限控制和请求日志的接入。3.4 验证代理类的类型与接口关系为了加深理解建议在测试代码里多打印几行类型信息System.out.println(代理类名: adminProxy.getClass().getName()); System.out.println(是否Proxy子类: (adminProxy instanceof Proxy)); System.out.println(是否实现OrderService: (adminProxy instanceof OrderService)); System.out.println(是否原实现类: (adminProxy instanceof OrderServiceImpl));输出会让你非常直观地理解代理对象的本质代理类名: com.sun.proxy.$Proxy0 是否Proxy子类: true 是否实现OrderService: true 是否原实现类: false这里有一个需要关注的点代理对象和OrderServiceImpl之间并没有继承关系它们都实现了OrderService接口但代理对象是Proxy的子类和目标类属于不同的继承体系。所以如果你在代码里直接把代理对象强转成OrderServiceImpl会抛出ClassCastException。很多同学第一次写动态代理在这里栽了跟头跟这个类型结构有直接关系。4. 源码深挖Proxy类到底做了什么4.1 代理类生成的核心流程要理解JDK动态代理的底层逻辑绕不开Proxy.newProxyInstance方法。我把JDK 8里这个方法的核心流程梳理一下public static Object newProxyInstance(ClassLoader loader, Class?[] interfaces, InvocationHandler h) { // 1. 校验InvocationHandler不为null Objects.requireNonNull(h); // 2. 拷贝接口列表确保安全 final Class?[] intfs interfaces.clone(); // 3. 权限检查确保调用方可以访问代理包 ... // 4. 查找或生成代理类 Class? cl getProxyClass0(loader, intfs); // 5. 通过反射获取构造函数参数就是InvocationHandler final Constructor? cons cl.getConstructor(constructorParams); // 6. 创建代理实例 return newInstance(cons, h); }关键在getProxyClass0方法它首先从缓存里查如果缓存中已经有这个类加载器和接口组合对应的代理类就直接返回不会再重复生成。这就是为什么多次调用newProxyInstance时如果接口列表和类加载器相同拿到的代理类class对象是同一个。如果缓存没有就调用ProxyClassFactory去生成。ProxyClassFactory的apply方法才是真正创建代理类字节码的地方。它做了几件事校验传入的接口是否对类加载器可见、是否是接口、是否重复。指定代理包名。如果接口在某个具体包下代理类会生成在该包的子包下否则默认用com.sun.proxy。计算代理类的名称也就是$Proxy0这种格式。调用ProxyGenerator.generateProxyClass生成Class文件再通过defineClass0加载。也就是说$Proxy0这个类从无到有的过程是在JVM内部通过字节码生成技术完成的。它不是一个普通的Java类文件放在classpath里而是在运行期直接生成的gradle字节数组再交给JVM定义成一个真正的Class对象。具体点说生成出来的代理类会包含这些内容对每个接口的每个方法都生成一个对应的public方法。每个方法内部调用父类Proxy的h字段所指向的InvocationHandler的invoke方法。额外实现hashCode、equals、toString三个Object方法同样转发给InvocationHandler。持有静态Method对象在类的静态初始化块中通过反射获取接口方法元数据。4.2 缓存机制与性能从上面的流程可以看出生成代理类本身是一个相对重的操作涉及字节码生成和类加载。但JDK做了缓存优化同一个类加载器和同一组接口只会生成一次代理类。所以你在项目里大量创建代理对象其实并不用太担心每秒new多个代理实例的性能问题因为类生成的开销只发生一次后续的new操作只是反射创建对象而已。真正需要关注的是方法调用层面的性能每调用一次代理方法都会额外经过invoke的反射转发和直接调用目标方法相比确实有开销但这个开销在绝大多数业务系统里可以忽略不计。只有当你在超高性能要求的场景下比如每秒百万次调用的网关才需要考虑这个层面的成本。4.3 一个容易忽略的jdk版本差异不同JDK版本对动态代理的实现细节有差异。在JDK 8及之前代理类包名固定为com.sun.proxy从JDK 9开始ProxyGenerator生成类的方式有所调整代理包名会基于接口所在的模块和包来计算比如jdk.proxy1这样的命名方式。所以如果你在代码里硬编码去判断代理类的包名跨JDK版本升级时会踩坑。建议只做instanceof判断不要依赖具体的类名和包名。另外从JDK 8开始Proxy类新增了一个静态方法isProxyClass(Class?)可以用来判断一个类是否是动态代理类。这个在框架开发中很有用比如做AOP时要避免对已经是代理对象的Bean再次代理就可以用这个方法来排查。5. JDK动态代理与CGLIB的对比5.1 实现机制的差异网上关于JDK动态代理和CGLIB的区别说法很多但核心就两条JDK动态代理基于接口通过实现接口的方式生成代理类代理类与目标类实现同一接口。CGLIB基于继承通过生成目标类的子类来创建代理代理类继承目标类并重写方法。这导致了一个直接的结果JDK动态代理只能代理接口方法CGLIB可以代理普通类和接口方法。但是CGLIB有一个天然的局限——对于final类它无法生成子类对于final方法它无法重写。所以这两者不是替代关系而是互补关系。CGLIB并不是JDK自带的而是一个第三方库通常作为Spring的依赖出现在项目里。Spring的AOP默认策略是目标Bean实现了接口就用JDK动态代理没有实现接口就用CGLIB。开发者可以通过proxyTargetClass配置项来强制指定用CGLIB。我把两者的关键差异整理成一个表格对比维度JDK动态代理CGLIB代理原理实现接口继承目标类生成子类目标要求必须实现至少一个接口不能是final类方法不能是final依赖JDK原生支持需要引入cglib依赖性能特点代理类生成快调用略慢代理类生成稍慢调用性能较好包名特征$Proxy0包含CGLIB特征的后缀如$$EnhancerByCGLIB$$适用场景大多数Spring Bean没有接口的类、需要强制代理类5.2 性能对比的误区不少同学以为CGLIB一定比JDK动态代理快这是一个需要纠正的认知偏差。早期的CGLIB在方法调用性能上确实优于JDK动态代理因为JDK动态代理的方法调用是通过反射执行的而CGLIB采用的是FastClass机制通过索引直接调用方法避开了反射的部分开销。但这里有几个前提需要说清楚。第一JDK从较早的版本开始就对反射做了大量优化在热点调用场景下反射调用经过JIT内联优化后性能已经非常接近直接调用。第二CGLIB创建代理类时需要生成字节码创建代理对象的开销是高于JDK动态代理的。第三现代Spring应用里绝大多数类都实现了接口默认走JDK动态代理就够了性能瓶颈也从来不在代理机制这一层。所以我的实际建议是不要盲目追求CGLIB跟着Spring的默认配置走。如果目标类确实没有接口再用CGLIB。5.3 结合Spring AOP看两者的选择逻辑Spring AOP在选择代理方式时有一个优先级判断逻辑。在Spring 5.x中默认配置下如果目标对象实现了接口则使用JDK动态代理否则使用CGLIB。如果你在配置类上加EnableAspectJAutoProxy(proxyTargetClass true)那么即使目标类实现了接口也会强制使用CGLIB。项目里出现Transactional不生效的情况很多就是代理方式没搞对。比如你在一个类内部通过this调用另一个方法this指向的是原始对象而不是代理对象事务注解就不会被代理拦截事务自然不生效。这个问题和JDK动态代理、CGLIB都有关系——只要你是通过this调内部方法两种代理都拦不住。解决办法是注入代理对象自身或者把内部调用拆到另外一个Bean里。还有一个常见场景你给一个类加了Transactional但类没有实现接口Spring只能走CGLIB。如果这个类恰好是final的CGLIB无法生成子类启动的时候就会报错。这种情况下要么去掉final修饰要么让类实现一个接口并切到JDK动态代理。6. 实际项目中的应用场景扩展6.1 框架底层的拦截器实现日常开发中我们大多数人是作为框架的使用者不太会直接写代理代码。但代理机制默默支撑着很多核心功能了解这些能帮你更快排查问题。Spring AOP的切面、MyBatis的Mapper接口实现、Feign的远程调用客户端、Retrofit的API接口适配这些框架底层都大量使用了JDK动态代理。以MyBatis为例它只需要定义Mapper接口框架通过JDK动态代理为每个接口方法生成代理对象调用时拦截方法名和参数转换成对应的SQL执行。所以MyBatis的Mapper为什么只能定义接口不能写实现类因为框架期望所有的实现逻辑都由代理来提供。Spring的Configuration类在被增强时如果不是特殊的Full模式走的就是CGLIB代理而普通Bean的AOP增强优先走JDK动态代理。排查Spring启动日志时如果你看到“Bean X is being proxied with CGLIB”这种日志基本上就能判断出目标Bean没有实现接口或者被强制指定了CGLIB模式。6.2 手写一个轻量级慢调用监控原理搞透之后写一个实用性很强的监控工具其实不难。比如我想监控Service层每个方法的调用耗时和异常次数如果写很多拦截器会很啰嗦用JDK动态代理可以做成一个通用的监控处理器。public class MonitorHandler implements InvocationHandler { private final Object target; private final String className; public MonitorHandler(Object target) { this.target target; this.className target.getClass().getSimpleName(); } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { long start System.nanoTime(); boolean success true; try { return method.invoke(target, args); } catch (InvocationTargetException e) { success false; throw e.getCause(); } finally { long costMills (System.nanoTime() - start) / 1_000_000; System.out.printf([监控] %s.%s 成功%s 耗时%dms%n, className, method.getName(), success, costMills); } } }再配合一个工厂方法让客户端只依赖接口完全不感知代理的存在public class MonitorProxyFactory { public static T T create(ClassT interfaceType, T target) { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class?[]{interfaceType}, new MonitorHandler(target) ); } }这个工具可以直接用在你突然想排查某个Service性能问题的场景不用改动业务代码临时在启动入口包一层就能看到每种方法的耗时输出。这种灵活性我觉得是代理机制最迷人的地方。6.3 事务代理的模拟实现再扩展一步用JDK动态代理模拟一个简单的声明式事务处理。核心思想是在invoke里捕捉方法上的Transaction注解使用自定义注解如果有的话开启事务、执行业务、提交或者回滚。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 needTransaction method.isAnnotationPresent(MyTransaction.class); if (!needTransaction) { return method.invoke(target, args); } TransactionManager.begin(); try { Object result method.invoke(target, args); TransactionManager.commit(); return result; } catch (Exception e) { TransactionManager.rollback(); throw e; } } }这个例子虽然简陋但你已经可以看到Spring Transactional的基础原型了注解作为标记代理拦截方法调用根据注解是否存在决定是否开启事务try-catch里决定提交还是回滚。后续要接真实的数据库事务把TransactionManager换成DataSourceTransactionManager即可。7. 常见问题与排查技巧实录7.1 ClassCastException代理对象无法强转为目标实现类这个现象出现得非常频繁。你写了类似这样的代码OrderServiceImpl impl (OrderServiceImpl) adminProxy;结果抛ClassCastException。原因前面已经说过代理对象是$Proxy0它和OrderServiceImpl之间没有继承关系。代理对象只实现了OrderService接口和Proxy的接口定义。解决办法也很简单编码时面向接口不要把代理对象强转成具体实现类。7.2 InvocationTargetException把业务异常包了一层在invoke方法里调用method.invoke(target, args)时如果目标方法抛出了异常这里不会直接抛出原始异常而是抛出包装后的InvocationTargetException。如果你不做任何处理调用方会看到这个莫名其妙的异常而不是业务上定义的异常类型。解决方案是在invoke里捕获InvocationTargetException取出真正的cause并重新抛出。如果目标方法声明了受检异常直接throw cause即可因为invoke方法签名本身声明throws Throwable。7.3 无限递归导致StackOverflowError新手经常在invoke方法里访问proxy的方法比如public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(proxy.toString()); // 这里会再次调用代理方法 return method.invoke(target, args); }proxy.toString()会再次进入invoke方法而invoke方法里又调用proxy.toString()死循环就产生了。要输出代理信息应该用method或target的信息来拼字符串或者调用静态方法直接判断不要轻易调用proxy的方法。7.4 防止框架对代理对象二次代理在一些框架集成场景里你可能需要判断一个对象是不是已经是代理。此时可以用Proxy.isProxyClass方法或者检查类名是否包含$Proxy。在Spring环境中更通用的做法是用AopUtils.isAopProxy来检测。如果在封装公共组件时遇到“代理了个代理”的情况加一个防重入判断往往能省掉很多烦恼。7.5 自定义注解丢失问题如果目标方法上的注解是在实现类的方法上通过method.getAnnotation获取时要注意你拿到的方法对象来自接口还是实现类。如果代理类是通过接口生成的那么method对应的是接口上的方法接口没有标注解就获取不到。这也是很多人在实现类方法上加了Log注解却发现不生效的原因。解决办法是优先获取被代理目标类的方法来读取注解或者在接口方法上同时标注。我这些年调试动态代理相关的问题几乎都是围绕上面这几个点展开的。很多时候报错信息看着吓人但只要把“代理类和目标类不是同一个类”这个意识刻在脑子里大部分问题都能顺藤摸瓜找到根因。如果你正准备系统掌握代理这块内容我建议自己动手写一遍示例再用IDE的debug功能分别在InvocationHandler的invoke、method.invoke、目标类方法内部打好断点跑一遍调用链。这一步做完你对Java代理的印象会比看十篇文章都深刻。后面要理解Spring AOP、自己封装中间件都会顺很多。