Java 的动态代理是怎么实现的?

2025年 阅读约 8 分钟 面试指南 · Java面试

深入解析Java动态代理的实现原理,从JDK代理到CGLIB,分初级、中级、高级三个层次全面讲解。

一句话总结

JDK 代理基于接口(Proxy + InvocationHandler,运行时生成 $Proxy 类)→ CGLIB 基于继承(ASM 生成子类,不能代理 final)。Spring Boot 2.x 默认 CGLIB(避免接口注入失效)。自调用陷阱:this.methodB() 绕过代理,需用 AopContext.currentProxy()。

初级理解

动态代理可以在运行时动态创建代理类,无需手动编写代理类代码。Java 中有两种主流实现:

JDK 动态代理:基于接口,使用 java.lang.reflect.ProxyInvocationHandler。被代理的类必须实现接口。

CGLIB 动态代理:基于继承,通过字节码技术生成目标类的子类作为代理。不需要接口,但不能代理 final 类和方法。

// JDK 动态代理 interface UserService { void save(); } class UserServiceImpl implements UserService { public void save() { System.out.println("保存用户"); } } // 代理处理器 class LogHandler implements InvocationHandler { private Object target; public LogHandler(Object target) { this.target = target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("开始日志..."); Object result = method.invoke(target, args); System.out.println("结束日志..."); return result; } } // 创建代理 UserService proxy = (UserService) Proxy.newProxyInstance( UserService.class.getClassLoader(), new Class[]{UserService.class}, new LogHandler(new UserServiceImpl()) ); proxy.save(); // 自动打印日志

中级深入

JDK 动态代理的底层原理:Proxy.newProxyInstance() 会在运行时动态生成一个代理类的字节码,这个代理类实现了指定的接口,并在每个方法调用时转发给 InvocationHandler.invoke()。

可以通过系统属性保存生成的代理类字节码:-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true(JDK 8)或 -Djdk.proxy.ProxyGenerator.saveGeneratedFiles=true(JDK 11+)。

CGLIB 的底层原理:通过 ASM 字节码框架生成目标类的子类,重写父类的非 final 方法,在方法调用前后插入增强逻辑。CGLIB 通过 MethodInterceptor 接口实现拦截。

对比JDK 动态代理CGLIB
原理基于接口基于继承
限制必须有接口不能代理 final 类/方法
性能创建快,调用略慢创建慢,调用快
依赖JDK 内置需要第三方库

高级拓展

Spring AOP 中的代理选择策略:

Spring AOP 默认使用 JDK 动态代理(如果目标类实现了接口),否则使用 CGLIB。Spring Boot 2.x 开始默认使用 CGLIB,可以通过 spring.aop.proxy-target-class=true/false 配置。

为什么 Spring Boot 默认用 CGLIB?因为 JDK 代理只能代理接口方法,如果注入的是实现类而非接口,代理会失效。CGLIB 代理实现类更符合开发习惯。

自调用失效问题:同一个类中方法 A 调用方法 B,如果 B 需要被代理增强,直接调用 this.B() 不会触发代理(因为绕过了代理对象)。解决方案:通过 AopContext.currentProxy() 获取代理对象再调用,或将方法拆分到不同 Bean 中。

@Service public class UserService { @Transactional public void methodA() { // 直接调用 methodB,事务不会生效! this.methodB(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void methodB() { ... } // 正确做法:通过代理调用 public void methodA() { ((UserService) AopContext.currentProxy()).methodB(); } }
面试加分项:能说出 Spring AOP 的代理选择策略和自调用失效问题,说明你有 Spring 实战经验。

实战场景

场景:用动态代理实现接口限流

// 限流注解 @Retention(RetentionPolicy.RUNTIME) @interface RateLimit { int value() default 100; // 每秒最大请求数 } // 限流代理处理器 class RateLimitHandler implements InvocationHandler { private final Object target; private final RateLimiter limiter = RateLimiter.create(100); public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { RateLimit rl = method.getAnnotation(RateLimit.class); if (rl != null && !limiter.tryAcquire()) { throw new RuntimeException("请求过于频繁,请稍后再试"); } return method.invoke(target, args); } }

场景:自调用失效的正确处理

// 方案1:通过 AopContext 获取代理 @Service public class UserService { public void methodA() { ((UserService) AopContext.currentProxy()).methodB(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void methodB() { /* ... */ } } // 方案2:自己注入自己 @Service public class UserService { @Autowired private UserService self; public void methodA() { self.methodB(); } }

面试模拟

Q:JDK 动态代理和 CGLIB 怎么选?

A:有接口用 JDK(JDK 内置,无额外依赖,创建快);无接口用 CGLIB(通过继承,调用快)。Spring 默认策略:实现了接口→JDK,否则→CGLIB。Spring Boot 2.x 默认 CGLIB,因为注入实现类更常见。

Q:为什么 @Transactional 自调用不生效?

A:Spring AOP 基于代理,外部调用时走代理对象触发拦截器。但类内部 this.methodB() 直接调用目标对象的方法,绕过了代理。解决方案:1) AopContext.currentProxy() 获取代理;2) 自己注入自己;3) 拆分成两个 Bean;4) 使用 AspectJ 编译期织入。