深入浅出 Java 动态代理:JDK Proxy 与 CGLIB 全方位对比
目录
四、AOP(尤其是Spring AOP)为何倾向于选择CGLIB?
2. 解决了“方法自调用(Self-Invocation)”时AOP失效的问题
引言
动态代理模式允许我们在程序运行时,为一个或多个目标对象动态地生成一个代理对象,这个代理对象可以在调用真实方法前后“做手脚”,从而实现功能的增强。在Java生态中,实现动态代理最主流的两种方案便是:JDK动态代理和CGLIB。
一、JDK 动态代理 - 来自官方的正统实现
JDK动态代理是Java官方在java.lang.reflect包中提供的原生支持。它的实现非常纯粹,但有一个核心前提:被代理的目标类必须实现至少一个接口。
1. 核心原理
JDK动态代理的核心是利用反射机制。它会在运行时创建一个新的代理类(例如 $Proxy0),这个代理类会实现目标类所实现的所有接口。当你通过代理对象调用接口方法时,这个调用会被转发到我们提供的一个InvocationHandler(调用处理器)的invoke方法中,从而给了我们执行增强逻辑的机会。
关键组件:
-
java.lang.reflect.Proxy:用于创建代理实例的核心工厂类。 -
java.lang.reflect.InvocationHandler:一个接口,我们需要实现它的invoke方法,所有对代理对象的调用都会被转发到这里。
2. 代码实战
让我们通过一个经典的“用户服务”场景来演示。
步骤1:定义接口
// IUserService.java
public interface IUserService {
void addUser(String username);
}
步骤2:创建目标实现类
// UserServiceImpl.java
public class UserServiceImpl implements IUserService {
@Override
public void addUser(String username) {
System.out.println("【核心业务】数据库中添加用户:" + username);
}
}
步骤3:创建调用处理器 (InvocationHandler)
// JdkProxyInvocationHandler.java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class JdkProxyInvocationHandler implements InvocationHandler {
// 被代理的目标对象
private final Object target;
public JdkProxyInvocationHandler(Object target) {
this.target = target;
}
/**
* @param proxy 代理对象本身
* @param method 正在被调用的方法
* @param args 方法的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("【JDK代理-前置通知】处理事务开始...");
// 通过反射调用目标对象的原始方法
Object result = method.invoke(target, args);
System.out.println("【JDK代理-后置通知】处理事务结束...");
return result;
}
}
步骤4:测试
// JdkProxyTest.java
import java.lang.reflect.Proxy;
public class JdkProxyTest {
public static void main(String[] args) {
// 1. 创建目标对象
IUserService target = new UserServiceImpl();
// 2. 创建调用处理器
InvocationHandler handler = new JdkProxyInvocationHandler(target);
// 3. 使用Proxy.newProxyInstance()创建代理对象
IUserService proxyInstance = (IUserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 类加载器
target.getClass().getInterfaces(), // 目标类实现的接口数组
handler // 调用处理器
);
// 4. 通过代理对象调用方法
proxyInstance.addUser("CSDN");
// 打印代理对象的类型
System.out.println("代理对象类型: " + proxyInstance.getClass().getName());
}
}
输出结果:
【JDK代理-前置通知】处理事务开始...
【核心业务】数据库中添加用户:CSDN
【JDK代理-后置通知】处理事务结束...
代理对象类型: com.sun.proxy.$Proxy0
二、CGLIB 动态代理 - 突破接口的限制
尽管JDK动态代理很方便,但它“必须实现接口”的限制在某些场景下显得很棘手。如果我们要代理一个没有实现任何接口的普通类怎么办?这时,CGLIB就登场了。
1. 核心原理
CGLIB通过一个名为ASM的底层字节码处理框架,在运行时动态地为目标类生成一个子类。这个子类会重写父类(目标类)中所有非final的方法。当我们调用代理对象的方法时,实际上是调用了这个子类中重写的方法。CGLIB通过一个MethodInterceptor(方法拦截器)来拦截这些调用。
关键组件:
-
net.sf.cglib.proxy.Enhancer:创建代理对象的核心类。 -
net.sf.cglib.proxy.MethodInterceptor:类似于InvocationHandler,用于拦截方法调用。
2. 代码实战
用一个例子来演示。
步骤1:添加依赖
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
步骤2:创建目标类(无接口)
// TrainStation.java
public class TrainStation {
public void sellTicket() {
System.out.println("【核心业务】火车站售票...");
}
public final void securityCheck() {
System.out.println("【核心业务】安检... (final方法)");
}
}
步骤3:创建方法拦截器
// CglibMethodInterceptor.java
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CglibMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("【CGLIB代理-前置通知】代售点收取服务费...");
// 通过MethodProxy调用父类(目标类)的原始方法
Object result = proxy.invokeSuper(obj, args);
System.out.println("【CGLIB代理-后置通知】代售点完成售票...");
return result;
}
}
步骤4:测试
// CglibProxyTest.java
import net.sf.cglib.proxy.Enhancer;
public class CglibProxyTest {
public static void main(String[] args) {
// 1. 创建Enhancer对象
Enhancer enhancer = new Enhancer();
// 2. 设置父类(目标类)
enhancer.setSuperclass(TrainStation.class);
// 3. 设置回调(方法拦截器)
enhancer.setCallback(new CglibMethodInterceptor());
// 4. 创建代理对象
TrainStation proxyStation = (TrainStation) enhancer.create();
// 5. 调用方法
proxyStation.sellTicket();
System.out.println("--- 分割线 ---");
// 调用final方法,观察是否会被代理
proxyStation.securityCheck();
System.out.println("代理对象类型: " + proxyStation.getClass().getName());
}
}
输出结果:
【CGLIB代理-前置通知】代售点收取服务费...
【核心业务】火车站售票...
【CGLIB代理-后置通知】代售点完成售票...
--- 分割线 ---
【核心业务】安检... (final方法)
代理对象类型: com.example.cglib.TrainStation$$EnhancerByCGLIB$$...
从结果可以看出,sellTicket被成功代理,而final方法securityCheck则直接调用了原始逻辑,未被拦截。
三、全方位对决:JDK Proxy vs. CGLIB
现在,我们用一张表格来清晰地总结它们之间的核心区别。
| 特性 | JDK 动态代理 | CGLIB 动态代理 |
| 实现原理 | 基于反射机制,动态实现接口 | 基于字节码增强(ASM),动态生成子类 |
| 代理前提 | 目标类必须实现至少一个接口 | 目标类不能是final,否则无法继承 |
| 代理范围 | 只能代理接口中定义的方法 | 可以代理类中所有非final的方法 |
| 外部依赖 | 无,是Java原生功能 | 需要引入cglib第三方库 |
| 性能 | 创建代理对象较快,方法调用涉及反射,早期版本较慢 | 创建代理对象较慢,方法调用通过FastClass机制,早期版本更快 |
| 现代性能 | 在JDK 1.8+,反射调用性能已大幅优化,与CGLIB差距微乎其微,甚至在某些场景下反超 | 优势不再明显,性能差异通常不是选择的关键 |
| Spring AOP | 如果目标类实现了接口,默认使用JDK代理 | 如果目标类没有实现接口,或配置了proxy-target-class=true,则使用CGLIB |
总结
JDK动态代理和CGLIB是Java动态代理技术中两座不可逾越的高山,它们各自的特点决定了其不同的适用场景。
-
JDK动态代理:作为Java的原生技术,它稳定且无需引入额外依赖。当你设计的系统遵循“面向接口编程”的最佳实践时,它无疑是首选。
-
CGLIB:它打破了接口的限制,使得代理普通类成为可能,极大地扩展了AOP的应用范围。这也是为什么在Spring Boot 2.x之后,即使有接口也默认优先使用CGLIB的原因之一,因为它能避免一些复杂的类型转换问题。
四、AOP(尤其是Spring AOP)为何倾向于选择CGLIB?
在现代的Spring Boot应用中,CGLIB已经成为事实上的默认和推荐选择。即便你的类实现了接口,Spring Boot也倾向于使用CGLIB。
这背后主要有以下几个关键原因:
1. 最大的优势:无需接口,为开发提供更大的灵活性
这是CGLIB最根本的优势。在大型项目中,并非所有的类都需要或适合抽象出接口。有时候一个类只是一个具体的实现,并没有多态的需求。如果强制要求所有需要AOP增强的类都必须实现接口,会给开发带来不必要的负担和额外的代码量。CGLIB让AOP的应用场景变得更加广泛和灵活。
2. 解决了“方法自调用(Self-Invocation)”时AOP失效的问题
这是一个非常重要且在面试中经常被问到的陷阱。
问题场景: 假设你有一个被代理的Service类,其中一个方法methodA()调用了同一个类中的另一个方法methodB(),并且methodB()上也配置了AOP通知。
@Service
public class MyService {
@Transactional // 假设这是一个AOP切面
public void methodB() {
// ... 数据库操作 ...
}
public void methodA() {
System.out.println("Executing methodA...");
// 重点看这里!
this.methodB(); // 或者直接调用 methodB()
}
}
-
如果使用JDK动态代理:外部调用
proxy.methodA()时,AOP生效。但当methodA()内部执行this.methodB()时,这里的this指向的是原始的目标对象,而不是代理对象。因此,这个调用是一个普通的内部方法调用,它绕过了代理,导致methodB()上的@Transactional切面完全失效。 -
如果使用CGLIB代理:CGLIB创建的是目标类的子类。代理对象本身就是增强后的对象。当
methodA()执行时,this指向的是CGLIB创建的子类实例。因此,调用this.methodB()时,调用的是子类中重写后的methodB(),这个调用依然会被AOP拦截,从而保证了切面逻辑的正确执行。
为了避免这种令人困惑的行为,并使AOP的作用更加一致和可预测,Spring Boot默认采用CGLIB,从根本上解决了自调用失效的问题。
3. 代理行为的一致性和类型转换问题
使用JDK代理时,从Spring容器中获取的Bean只能被强制类型转换为它所实现的接口类型。如果你试图将它转换为它的实现类类型,会抛出ClassCastException。
// MyServiceImpl implements IMyService
IMyService service = context.getBean(IMyService.class); // 正确
MyServiceImpl serviceImpl = (MyServiceImpl) service; // 使用JDK代理时会抛出异常!
而使用CGLIB,由于代理对象是目标类的子类,所以它既可以被转换为接口类型,也可以被安全地转换为其父类(即目标实现类)的类型。默认使用CGLIB可以保证应用中所有代理对象的行为一致,避免了这类潜在的类型转换风险。
结论: Spring AOP之所以选择并默认推荐CGLIB,是因为它更强大、更灵活、行为更一致,并且解决了JDK代理中令人头疼的“自调用失效”问题,使得AOP的应用更加健壮和符合直觉。
更多推荐

所有评论(0)