新闻详情

新闻详情

首页 / 资讯中心 / 详情

JDK动态代理原理与实战:从InvocationHandler到Spring AOP

发布时间:2026/10/1 9:47:58来源:尧图网络
JDK动态代理原理与实战:从InvocationHandler到Spring AOP
1. 动态代理到底解决了什么问题——从静态代理的痛点说起先从一个真实的场景说起。前几年我在维护一个老项目里面十几个Service类、上百个业务方法每次改动都要往方法里塞日志。最开始我是这么干的在方法开头打一行System.out.println(开始执行...)方法结束再打一行。听起来挺简单但做了一次之后我就发现不对——日志逻辑散落在每个业务方法里改个日志格式要全局替换排查问题的时候日志和业务代码混在一起根本分不清主次。更难受的是类似的需求还不止日志事务管理要加、权限校验要加、性能统计要加每一种横切逻辑都要把所有方法翻一遍。这就是动态代理出现的核心动机把那些跟核心业务无关、但又不得不做的公共逻辑从业务代码里剥离出来做成一个可以“插入”到方法调用链上的处理器。Java里最早解决这个问题的方式是静态代理但静态代理有一个天生的毛病——每代理一个接口你就要写一个对应的代理类。十个接口就是十个类每个类里全是机械重复的转发代码。动态代理则完全不同它是在运行时“现场生成”一个代理类你只需要写好一个通用的调用处理器系统就能自动为任意多个接口创建代理对象。这也是为什么Spring AOP、MyBatis的MapperProxy、Retrofit的动态接口实现等主流框架都离不开它。这篇文章我想把JDK动态代理从“是什么”“为什么这么设计”到“怎么写”“底层怎么工作”完整过一遍最后再聊一聊它和CGLIB的区别。无论你是刚接触代理模式的初学者还是已经用过Spring但没深入过源码的开发者我都尽量用能直接落地的方式讲清楚。1.1 代理模式是什么一个非常直观的生活类比要理解代理先举一个场景你想租房子但你又不想自己一家一家找房东、比价格、谈合同于是你找了中介。中介做的事情是什么表面上看中介“代替”房东把房子租给了你但你付的每一笔租金、签的每一份合同最终都会落到真房东那里。中介在中间可以做很多事带你看房、核实房源真实性、替你砍价甚至你中途想退租中介帮你协调。这个场景里的“中介”放到Java里就是代理对象那个“真房东”就是被代理的目标对象。你作为调用方只跟中介打交道完全不需要知道房东是谁。中介怎么找到的房子、怎么谈的价格对你来说都是透明的——只要中介对外提供的行为租给你房子跟真房东一致就行。用术语来说就是代理对象和目标对象实现相同的接口代理对象内部持有目标对象所有外部调用先经过代理代理在调用目标方法前后插入附加逻辑。1.2 经典静态代理写法一个问题解决新问题又来了静态代理是最简单的代理实现方式。比如我有一个接口public interface UserService { String getUserById(Long id); }目标对象就是真实的业务实现public class UserServiceImpl implements UserService { Override public String getUserById(Long id) { // 模拟查询数据库 return 用户- id; } }现在我要给这个接口加日志写一个静态代理类public class UserServiceStaticProxy implements UserService { private final UserService target; public UserServiceStaticProxy(UserService target) { this.target target; } Override public String getUserById(Long id) { System.out.println([静态代理] 调用前查询用户id id); String result target.getUserById(id); System.out.println([静态代理] 调用后查询结果 result); return result; } }调用方这样用UserService service new UserServiceStaticProxy(new UserServiceImpl()); String user service.getUserById(1001L);看到了吧代码一点儿都不复杂逻辑也对。但问题在于如果项目里有OrderService、ProductService、PayService每个接口都要加日志你得为每个接口各写一个代理类代理类和目标类几乎是1:1对应的。如果哪天接口里加了一个方法代理类也得同步加一个方法。这个维护成本会随着接口数量线性增长而且代理类里面全是机械重复的转发逻辑代码看起来特别冗余。很多初学者觉得“静态代理也没多麻烦”的原因是只写了一个接口。真实的项目里接口数量一多你马上就会感受到什么叫“代理类的类爆炸”。2. JDK动态代理的三件套接口、InvocationHandler与ProxyJDK动态代理的设计很有意思它把静态代理里“每个接口写一个代理类”的问题抽象成了三个角色的协作业务接口、调用处理器InvocationHandler、Proxy工具类。你只需要专注于写一份通用的“代理拦截逻辑”剩下的“如何生成代理类”“如何把调用转发给拦截器”全部交给JDK内部完成。2.1 为什么JDK动态代理非要“基于接口”不可这是面试里被问烂了、但很多人没真正理解的一个点。JDK动态代理生成的代理类是通过继承Proxy这个父类然后实现你指定的业务接口来创建的。Java是单继承的代理类已经继承了Proxy就不可能再去继承某个普通的业务类了。所以JDK动态代理只能代理接口不能直接代理一个具体的类。这个设计有它的合理性接口代表一种契约代理类和目标类都实现同一个接口外部调用方根本无需知道背后到底是真实对象还是代理对象天然符合面向接口编程的习惯。而且有了接口代理类就可以在运行时动态生成编译期根本不需要预先知道。反过来如果你想代理一个没有接口的普通类JDK动态代理就无能为力了。这时候要么你手动给这个类抽象出接口来要么就用CGLIB基于继承的方式生成代理子类。这个对比后面第6章会专门展开。2.2 InvocationHandler所有代理逻辑的唯一入口InvocationHandler是java.lang.reflect包下的一个接口它只有一个方法public interface InvocationHandler { Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }这个方法就是整个动态代理的核心入口。当外部调用代理对象的任何方法时都不会直接执行目标对象的代码而是先进入这个invoke方法。三个参数分别代表proxy生成的代理对象本身。一般用不到但在某些场景比如方法里需要调用代理对象自己的其他方法会用到。method当前被调用的方法在反射层面的Method对象。通过它我们可以拿到方法名、参数类型、注解等信息也可以用它调用目标对象上的真实方法。args调用方法时传入的参数数组。你可能会想“所有方法都进同一个invoke方法那不就跟一个大switch一样吗”这是动态代理一个非常有用的特性你用一份统一的代码处理所有方法至于某个方法要不要拦截、拦截前做什么、拦截后做什么完全可以由你在这个方法内部自己决定。比如方法名以query开头就不加日志、方法上标了某个注解就做权限校验等。2.3 Proxy.newProxyInstance的三个参数逐一拆解Proxy类提供了静态方法newProxyInstance来生成代理对象。它的签名是public static Object newProxyInstance(ClassLoader loader, Class?[] interfaces, InvocationHandler h)很多人第一次看到这三个参数会很懵我逐一拆开讲第一个参数loader是类加载器。代理类是在运行时动态生成的新类JVM需要用一个类加载器把它加载进来。这里通常传入目标接口的类加载器即目标接口.class.getClassLoader()。它可以简单理解为“由谁来把这个新生成的Class文件读进JVM”。第二个参数interfaces是接口数组。你需要告诉JDK我生成的这个代理类要“假装成”哪些接口的实现类。比如你要代理UserService接口就传入new Class[]{UserService.class}。可以有多个接口生成的代理类会同时实现它们。第三个参数h就是上面说过的InvocationHandler。它是代理逻辑的核心代理对象上所有方法调用最终都会交给它处理。返回的虽然是一个Object但实际运行时它是一个实现了UserService接口的代理对象所以可以直接强转成UserService来用。3. 手写一个真实的JDK动态代理示例从日志切面到跑通这一章我们走一个完整的例子。不搞花架子就实现一个真实业务里最常见的场景给Service层统一加日志和耗时统计。3.1 先定义接口和真实实现类依然是UserService这次我加一个方法进去让示例更接近真实public interface UserService { String getUserById(Long id); int createUser(String name); }实现类比较简单就不模拟数据库了直接返回固定值public class UserServiceImpl implements UserService { Override public String getUserById(Long id) { // 模拟耗时操作 sleep(50); return 用户- id; } Override public int createUser(String name) { sleep(30); System.out.println(真实创建用户: name); return 1; } private void sleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }3.2 实现InvocationHandler把横切逻辑写进invoke里现在写核心的调用处理器。目标是对接口里所有方法统一做三件事调用前置日志、执行真实方法、调用后置日志和耗时统计。import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; 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()); // 调用目标对象的真实方法args是入参 Object result method.invoke(target, args); long cost System.currentTimeMillis() - start; System.out.println([动态代理] 调用后方法 method.getName() 耗时 cost ms 返回 result); return result; } }这一段就是“切面逻辑”的雏形。method.invoke(target, args)这一行本质上就是用反射去执行目标对象上对应的真实方法。如果你不希望某个方法被调用了可以不执行method.invoke直接返回一个默认值这就是一个最简单的Mock工具。3.3 生成代理对象并调用验证结果接下来测试import java.lang.reflect.Proxy; public class DynamicProxyDemo { public static void main(String[] args) { // 1. 创建目标对象 UserService target new UserServiceImpl(); // 2. 创建调用处理器 LogInvocationHandler handler new LogInvocationHandler(target); // 3. 生成代理对象 UserService proxy (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, handler ); // 4. 通过代理对象调用方法 System.out.println(代理对象class proxy.getClass()); System.out.println(代理对象是否实现了UserService (proxy instanceof UserService)); System.out.println(); String user proxy.getUserById(1001L); System.out.println(最终结果: user); System.out.println(); int count proxy.createUser(张三); System.out.println(最终结果: 影响行数 count); } }跑一下输出如下代理对象classclass com.sun.proxy.$Proxy0 代理对象是否实现了UserServicetrue [动态代理] 调用前方法getUserById [动态代理] 调用后方法getUserById耗时52ms返回用户-1001 最终结果: 用户-1001 [动态代理] 调用前方法createUser 真实创建用户: 张三 [动态代理] 调用后方法createUser耗时31ms返回1 最终结果: 影响行数1注意这行输出代理对象classclass com.sun.proxy.$Proxy0。这个$Proxy0就是JDK在运行时动态生成的代理类它不属于任何一个源文件而是JVM在内存里直接生成并加载的。你每调用一次newProxyInstance只要接口组合相同通常会复用同一个代理类如果是新的接口组合就会再生成一个新的。3.4 进阶如何用一套处理器给多个接口做代理动态代理最大的好处就是你不需要为每个接口重复写处理器。同样的LogInvocationHandler完全可以复用到OrderService、ProductService上。比如OrderService orderProxy (OrderService) Proxy.newProxyInstance( OrderService.class.getClassLoader(), new Class[]{OrderService.class}, new LogInvocationHandler(new OrderServiceImpl()) );为什么能复用因为LogInvocationHandler里持有的是Object target调用目标方法用的是method.invoke(target, args)。不管什么接口方法调用的转发逻辑完全一样。这就是动态代理相对于静态代理的巨大优势一份处理器代理N个接口。如果你愿意甚至可以写一个通用的ProxyFactory通过泛型把生成代理的步骤收拢起来public class ProxyFactory { SuppressWarnings(unchecked) public static T T createProxy(T target, InvocationHandler handler) { return (T) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); } // 用法 // UserService proxy ProxyFactory.createProxy(new UserServiceImpl(), new LogInvocationHandler(target)); }这里注意target.getClass().getInterfaces()返回的是目标对象实现的全部接口这样就不用一个个手写接口数组了。4. 源码级拆解代理类到底是怎么“凭空”生成出来的示例跑通了但很多人这时候心里会有疑问那行class com.sun.proxy.$Proxy0到底是什么时候生成的它长什么样为什么调用代理对象的方法就会走进invoke方法4.1 Proxy.newProxyInstance的完整调用链打开JDK源码看看。Proxy.newProxyInstance方法的实现大致是这样的public static Object newProxyInstance(ClassLoader loader, Class?[] interfaces, InvocationHandler h) { // 1. 各种非空校验 // 2. 根据类加载器和接口查找或生成代理类 Class? cl getProxyClass0(loader, interfaces); // 3. 通过反射调用代理类的构造器传入 InvocationHandler final Constructor? cons cl.getConstructor(constructorParams); // constructorParams {InvocationHandler.class} return cons.newInstance(new Object[]{h}); }第二步的getProxyClass0内部做了缓存判断如果同一个类加载器和同一个接口组合已经生成过代理类就直接从缓存里取否则会调用ProxyClassFactory来真正生成代理类字节码。这个字节码生成工作最终落到ProxyGenerator.generateProxyClass方法上它会在内存中生成一个完整的Class文件字节数组然后通过loader.defineClass把它变成一个真正的Class对象。整个链路一句话归纳Proxy.newProxyInstance- 找类或生成类 - 反射拿到构造器 - new一个代理对象出来。4.2 代理类继承Proxy、实现业务接口的本质原因我们可以把代理类的字节码保存下来直接看它的真实结构。在启动JVM时加这个参数-Dsun.misc.ProxyGenerator.saveGeneratedFilestrue或者在新版JDK里这样-Djdk.proxy.ProxyGenerator.saveGeneratedFilestrue运行一次上面那个Demo工作目录下会出现com/sun/proxy/$Proxy0.class。用反编译工具看一下核心结构大致是这样的public final class $Proxy0 extends Proxy implements UserService { private static Method m1, m2, m3, m4, m0; public $Proxy0(InvocationHandler h) { super(h); } static { try { m1 Class.forName(java.lang.Object).getMethod(equals, Class.forName(java.lang.Object)); m2 Class.forName(java.lang.Object).getMethod(toString); m3 Class.forName(...UserService).getMethod(getUserById, Class.forName(java.lang.Long)); m4 Class.forName(...UserService).getMethod(createUser, Class.forName(java.lang.String)); m0 Class.forName(java.lang.Object).getMethod(hashCode); } catch (NoSuchMethodException e) { throw new NoSuchMethodError(e.getMessage()); } } Override public String getUserById(Long id) { try { return (String) super.h.invoke(this, m3, new Object[]{id}); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } Override public int createUser(String name) { try { return (int) super.h.invoke(this, m4, new Object[]{name}); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } Override public String toString() { // 同样交给 super.h.invoke 处理 } }这个结构解释了一切代理类extends Proxy而Proxy类里有一个受保护的字段h类型就是InvocationHandler。我们构造代理类时传入的handler最终就是给这个h字段赋值。代理类implements UserService所以它能被强转成UserService。代理类的每一个方法体几乎一模一样拿当前方法对应的Method对象调用super.h.invoke(this, mX, args)。这印证了前面说的——所有方法调用都会被路由到我们的InvocationHandler.invoke方法上。所以“代理对象调用方法时为什么一定会经过invoke”这个问题答案在字节码层面非常清晰不是JVM偷偷转了而是代理类的方法实现本身就只干了一件事——把调用转交给h.invoke。4.3 类加载器在这里扮演了什么角色很多人觉得ClassLoader参数很抽象。其实它的角色很单纯代理类是一个新的Class总得有“人”把它加载进JVM。这个“人”就是我们传入的loader。为什么要用目标接口的类加载器而不是随便一个因为代理类要实现目标接口而JVM里判断两个类是否兼容类加载器是重要依据。如果代理类和接口使用不同的类加载器可能会出现类型不兼容的问题。用同一个类加载器是最稳妥的选择。这也是为什么Spring AOP内部在决定用JDK代理还是CGLIB时总是优先取beanClass.getClassLoader()来传递。4.4 代理类只会生成一次之后会缓存这里有一个性能相关的细节同一个类加载器加上同一组接口对应的代理类JVM只会生成一次之后的调用直接复用缓存的Class。这一点在源码里体现得很明显Proxy内部维护了一个WeakCachekey就是类加载器和接口列表的组合。所以不要担心频繁调用newProxyInstance会疯狂生成新类——实际创建对象的开销是很小的。不过要注意缓存的存在也意味着如果动态生成大量的接口组合WeakCache里会积累大量代理类这些代理类所占用的元空间是需要回收的。在极端动态场景比如每次请求都生成一个新的接口实现下要注意元空间的监控。5. JDK动态代理的边界与高频坑为什么有的类就是代理不了用JDK动态代理这几年我遇到过不少让人抓狂的问题。这一章把高频坑整理出来每一条都是真实踩过的。5.1 没有接口的类直接报错现场与排查最经典的一个错。如果你对普通类直接代理public class UserServiceImpl { // 注意没有 implement UserService public String getUserById(Long id) { return 用户- id; } } // 生成代理时 UserServiceImpl proxy (UserServiceImpl) Proxy.newProxyInstance( UserServiceImpl.class.getClassLoader(), new Class[]{UserServiceImpl.class}, // 传了一个类而不是接口 handler );运行时会直接抛异常Exception in thread main java.lang.IllegalArgumentException: com.example.UserServiceImpl is not an interface at java.lang.reflect.Proxy$ProxyClassFactory.apply(Proxy.java:...原因前面解释过了代理类已经继承了ProxyJava单继承机制决定了它不能再继承一个普通类所以ProxyClassFactory只接受接口类型。遇到这个问题排查思路有三个确认传入interfaces数组的元素是不是接口。经常有人把目标类的Class对象传进去但实际上应该传它实现的接口。如果类确实没有接口考虑给这个类抽象出接口这也是比较推荐的做法如果不行改用CGLIB。查看Configuration代理或Spring AOP中的报错往往本质也是这个原因。5.2 invoke方法里返回类型不一致导致强转异常第二个很隐蔽的坑代理类字节码里每个方法体都有强转。反编译代码里可以看到(String) super.h.invoke(...)、(int) super.h.invoke(...)这样的强转。这意味着如果你的InvocationHandler.invoke方法返回了一个跟目标方法返回类型不兼容的对象运行时会抛出ClassCastException。举个例子createUser接口方法返回的是int基本类型代理类会做(int)强转。如果我在invoke里不小心返回了一个LongObject result method.invoke(target, args); return Long.valueOf(1L); // 应该返回 int实际返回了 Long运行时就会报ClassCastException: java.lang.Long cannot be cast to java.lang.Integer在拆箱环节可能会显示其他表现形式。这种错误一般出现在你写通用逻辑时对返回值的类型判断不严谨。我的经验是invoke方法里的返回逻辑尽量原样返回目标方法的执行结果。你确实可以改变返回值来“做手脚”但必须保证新类型能强转成接口方法声明的返回类型。5.3 在invoke里调用proxy的方法会怎么样前面提过invoke方法的第一个参数proxy是代理对象本身。如果你在invoke方法里写下proxy.toString()或proxy.hashCode()会发生什么看起来是“代理对象调用自己的方法”但实际上这段代码会再次路由到invoke方法形成递归调用。如果递归没有终止条件最终栈溢出。一个非常容易踩的场景有人在invoke里写了类似这样的日志Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(proxy.getClass()); // 这里触发了 getClass 方法但 getClass 不走动态代理 // ... }getClass是Object的final方法代理类没有重写它所以不会递归。但如果换成toString()、hashCode()或者equals()就会触发代理的方法调用从而进入invoke再次执行到toString()无限循环。所以在invoke里尽量避免直接操作proxy的toString、hashCode、equals方法。如果你需要拿到代理类的类型信息用proxy.getClass()没问题因为它不经过动态代理路由。5.4 被代理方法抛异常时invoke要处理好包装再讲一个异常相关的坑。反编译的代理类代码里有一段} catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException(e); }这意味着如果你在invoke方法里调用目标方法时目标方法抛了一个受检异常checked exception而这个受检异常没有在接口方法签名里声明那么它会被包装成UndeclaredThrowableException继续抛出。也就是说即使目标方法内部抛了一个普通的业务异常也有可能被包装成UndeclaredThrowableException导致调用方看起来“很难受”。实际排查思路是先看目标方法上的异常声明。如果你自定义了一个业务异常请把它声明在接口方法上。这样在代理方法路由时才能正常传播而不被包装。很多基于mybatis、spring的代理项目里那些“莫名其妙”的异常包裹问题根源往往都在这里。6. JDK动态代理与CGLIB的选型对比结合Spring AOP说清楚每次聊到动态代理必被问到的问题就是“JDK动态代理和CGLIB到底有什么区别为什么Spring有时用JDK有时用CGLIB”这一章我系统对比一下并给出实际选型建议。6.1 两者的实现机制差异一句话总结JDK动态代理基于接口代理类在运行时生成继承Proxy、实现目标接口方法调用通过InvocationHandler转发。CGLIB基于继承代理类通过继承目标类来生成一个子类然后重写父类的方法在重写的方法里加入增强逻辑底层使用ASM字节码生成框架直接操作字节码。两者的根本区别就是一个靠接口一个靠继承。由于Java单继承一个类只能被继承一次所以CGLIB要求目标类不能被final修饰目标方法也不能是final的。另外CGLIB可以代理没有接口的类这是它相对于JDK动态代理的最大优势。6.2 实际使用中的取舍接口优先还是类优先从开发规范角度我是强烈建议大家优先面向接口编程的。这不只是为了能用JDK动态代理更是为了代码的整洁和可替换性。如果一个类有接口直接优先用JDK动态代理如果一个类确实没有接口或者你不想为了代理去额外添加接口再考虑CGLIB。性能上早期版本CGLIB比JDK动态代理快因为反射调用慢但从JDK 8以后JDK动态代理经过优化两者的性能差距已经很小了。在实际业务系统里这种差距基本可以忽略。相反CGLIB创建代理对象时因为要生成子类字节码首次创建开销往往比JDK动态代理大。所以选型时机制和代码结构的适配性远比那一点点性能差距重要。下面用表格对比一下对比维度JDK动态代理CGLIB实现方式代理类继承Proxy实现目标接口代理类继承目标类重写方法对接口要求必须基于接口不要求接口但目标类不能是final被代理方法接口中声明的方法才可被代理非final、非static方法可被代理底层技术java.lang.reflect.Proxy InvocationHandlerASM字节码生成子类生成代理对象速度通常更快可缓存复用首次生成稍慢调用性能JDK8后经过优化差距很小略快或接近适用范围面向接口的项目没有接口、无法改造的场景代表框架Spring AOP默认MyBatis Mapper代理Spring AOP类代理Hibernate延迟加载等6.3 在Spring AOP中它们是如何配合工作的Spring AOP本身没有重新发明代理机制它是在运行时决定用JDK动态代理还是CGLIB来生成代理对象。默认规则是目标类实现了接口就用JDK动态代理目标类没有接口就用CGLIB。手动配置里可以强制proxy-target-classtrue让Spring无论什么情况都用CGLIB。这也解释了为什么很多人用Spring时会遇到一个现象自己写的类没有接口但加了Transactional注解后发现事务不生效或者代理不生效。排查的时候先确认一下这个类是不是被代理到了。如果一个类的某个方法是final的使用CGLIB时代理类无法重写这个方法这个方法的操作就不会进入代理拦截逻辑。这一点在给某个类的某个方法加事务或日志时特别容易踩坑。6.4 我的选型建议与踩坑结论结合这几次的真实项目经验我的结论很直接新写的业务代码能抽象接口就抽象接口优先用JDK动态代理。不是因为JDK天生比CGLIB好而是接口本身就是一种设计约束它能帮你保持代码的结构清晰。现有的老代码没有接口不想做大规模重构用CGLIB是最现实的选择。Spring里改一个配置就能切换但要注意final方法无法被代理。对于框架开发场景比如写一个通用Starter、一个SQL映射器、一个远程调用SDK通常也是优先JDK动态代理。MyBatis的Mapper接口就是典型——没有任何实现类直接通过Proxy.newProxyInstance给接口生成一个代理对象所有方法调用都被MapperProxy拦截最终路由到SQL执行器。最终极的建议是你要理解这两者的机制差异而不是“背结论”。因为今天你用的是某个框架的默认行为明天换个版本、换个框架默认行为可能就变了。比如Spring Boot 2.x及以后默认开启proxy-target-classtrue说明框架也在逐渐向CGLIB倾斜但如果你懂原理这种变化对你只是配置层面的小事而不是一个“黑盒谜团”。最后分享一个排查小技巧如果你不确定一个Bean到底有没有被代理、是哪种代理可以继续用前面的dumpClass方式或者直接在Spring容器里打印bean.getClass()看它是com.sun.proxy.$ProxyX还是com.example.UserService$$EnhancerBySpringCGLIB$$xxx。这一眼就能判定当前代理方式排查AOP失效问题会少走很多弯路。JDK动态代理这套东西看起来只是几个类的配合但理解了它你就理解了Spring AOP、MyBatis Mapper代理、Retrofit动态API实现等一大堆框架的核心底座。回头再看这些框架你会觉得所有“魔法”其实都是普通Java类在做老老实实的工作——只是它们借助动态代理把一个横切逻辑优雅地插到了每一次方法调用里。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2026 数学建模国赛 B 题复盘:从“不会下手”到拟推国一,我们做对了什么? 2026/10/1 10:29:37

2026 数学建模国赛 B 题复盘:从“不会下手”到拟推国一,我们做对了什么?

注:本文主要分享建模思路、比赛过程和论文打磨经验。为避免影响后续学习与交流,部分核心算法、关键参数、完整公式及代码细节不作公开。【配图1:文章背景图】一、先说结果:这是一次比预想中更“工程化”的建模今年参加数学建模国赛…

阅读更多 →
短视频脚本怎么写才不拖进度:从选题到口播稿的生产流程 2026/10/1 10:29:37

短视频脚本怎么写才不拖进度:从选题到口播稿的生产流程

短视频脚本怎么写才不拖进度:从选题到口播稿的生产流程 晚上 11 点,剪辑时间线已经铺开,脚本却还停在一句“大家好,今天我们来聊聊……”上。真正拖慢短视频项目的,往往不是写不出字,而是选题、素材和口播稿…

阅读更多 →
面试中 ThreadLocal 能问的,都在这了:2万字详解 2026/10/1 10:29:37

面试中 ThreadLocal 能问的,都在这了:2万字详解

一、从一个经典面试题说起在 Java 后端面试中,ThreadLocal 几乎是必考知识点。面试官经常会用下面这几个问题来试探候选人对并发编程和 JVM 内存模型的理解深度:ThreadLocal 有什么用?线程之间数据是隔离的吗?ThreadLocal 的底层实…

阅读更多 →
YuE2-3B 实操指南:从歌词与风格提示生成可编辑乐谱的完整歌曲 2026/10/1 10:29:37

YuE2-3B 实操指南:从歌词与风格提示生成可编辑乐谱的完整歌曲

音乐生成大模型人工智能媒体生成 【免费下载链接】YuE2-3B 项目地址: https://ai.gitcode.com/hf_mirrors/m-a-p/YuE2-3B 点击查看 免费下载 YuE2 是一个开源音乐生成模型,能够把一段歌词与一个风格提示词(style prompt)转化为带…

阅读更多 →
【面朝大厂】面试官:手写一个必然死锁的例子 2 万字详解 2026/10/1 10:29:37

【面朝大厂】面试官:手写一个必然死锁的例子 2 万字详解

一、面试开场:一道看似简单却暗藏玄机的题在多线程并发编程的面试中,有一道题几乎年年出现,它看起来只需要写几十行代码,却能一路追问到操作系统原理、JVM 锁实现、线上故障排查甚至分布式系统设计。这道题就是:请你手…

阅读更多 →
Jev是什么?如何把Jev模型接入TraeCode实现AI编程 2026/10/1 10:29:30

Jev是什么?如何把Jev模型接入TraeCode实现AI编程

最近后台私信里被问到最多的一个词就是“Jev”。不管是技术群、AI 编程社区还是推特时间线,都在说“Jev 爆了”“斯坦福有人拿 Jev 搭数据系统”“Jev 在 Codex 里跑得很顺”。但你要真去搜“Jev 是什么”,能搜到的正经解释又少得可怜,大部分…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉