深入浅出分析JDK动态代理与CGLIB动态代理的区别
动态代理是Java开发中非常重要的技术,尤其在AOP(面向切面编程)中应用广泛,比如Spring框架。JDK动态代理和CGLIB动态代理是两种常见的实现方式,它们各有特点。今天我们就来循序渐进地拆解它们的区别,顺便解决你"记不住API"的困扰------别担心,我们不死扣API,而是抓重点!
一、什么是动态代理?
先从概念说起。动态代理就是在程序运行时,通过某种机制动态生成一个代理对象,这个对象可以替目标对象执行方法,同时还能"加点料"(比如日志、事务控制)。想象一下,代理对象就像个中介,帮你干活还能顺便加点自己的小动作。
Java中有两种主流动态代理:
- JDK动态代理:Java自带,基于接口实现。
- CGLIB动态代理:第三方库,基于继承实现。
接下来,我们一步步拆解它们的区别。
二、JDK动态代理:基于接口的"正统"方式
工作原理
JDK动态代理的核心是java.lang.reflect.Proxy
类。它通过运行时生成一个实现了目标接口的代理类,来完成代理功能。具体步骤是:
- 你得有个接口(比如
UserService
)。 - 目标类(比如
UserServiceImpl
)实现这个接口。 - 用
Proxy.newProxyInstance
生成代理对象,指定接口和一个InvocationHandler
(处理器)。 - 调用代理对象的方法时,实际会交给
InvocationHandler
的invoke
方法执行。
简单例子
假设我们有个接口和实现类:
java
public interface UserService {
void sayHello();
}
public class UserServiceImpl implements UserService {
public void sayHello() {
System.out.println("Hello!");
}
}
用JDK动态代理生成代理:
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class JdkProxyDemo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before...");
Object result = method.invoke(target, args); // 通过反射调用目标方法
System.out.println("After...");
return result;
}
}
);
proxy.sayHello();
}
}
输出:
erlang
Before...
Hello!
After...
InvocationHandler的invoke参数
proxy
:代理对象本身。method
:被调用的目标方法。args
:方法的参数数组。
注意,这里需要手动传入target
(目标对象),通过method.invoke(target, args)
反射调用。
特点
- 基于接口:必须有接口,没接口它就无能为力。
- 用反射 :
invoke
方法通过反射调用目标方法,性能上稍微慢一点。 - 官方支持:JDK自带,无需额外依赖。
三、CGLIB动态代理:基于继承的"灵活"方式
工作原理
CGLIB(Code Generation Library)是第三方库,它通过字节码操作(ASM框架)在运行时生成目标类的子类作为代理类。代理类会重写父类的方法,并在其中插入额外的逻辑。
简单例子
假设我们有个类(注意,不需要接口):
java
public class UserServiceImpl {
public void sayHello() {
System.out.println("Hello!");
}
}
用CGLIB生成代理:
java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class CglibProxyDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserServiceImpl.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before...");
Object result = proxy.invokeSuper(obj, args); // 调用父类方法
System.out.println("After...");
return result;
}
});
UserServiceImpl proxy = (UserServiceImpl) enhancer.create();
proxy.sayHello();
}
}
输出:
erlang
Before...
Hello!
After...
MethodInterceptor的intercept参数
obj
:代理对象本身(CGLIB生成的子类实例)。method
:被拦截的目标方法(java.lang.reflect.Method
类型)。args
:方法的参数数组,和JDK的args
一样。methodProxy
:CGLIB特有的MethodProxy
对象,封装了对父类方法的快速调用方式,通过invokeSuper
直接调用,避免反射。
重点 :methodProxy
是CGLIB效率高的关键。它不像JDK那样依赖反射,而是通过字节码直接调用父类方法。
特点
- 基于继承 :通过生成子类实现代理,所以目标类不能是
final
。 - 无需反射:直接调用父类方法,效率更高。
- 无需接口:不像JDK动态代理强制要求接口,CGLIB更灵活。
四、核心区别对比
维度 | JDK动态代理 | CGLIB动态代理 |
---|---|---|
实现方式 | 基于接口(通过Proxy 类) |
基于继承(生成子类) |
性能 | 用反射,稍慢 | 字节码操作,效率更高 |
依赖 | JDK自带,无需额外库 | 需要CGLIB库(Spring已集成) |
适用场景 | 必须有接口 | 无接口也能用,但不能是final类 |
回调参数 | proxy, method, args |
obj, method, args, methodProxy |
调用方式 | method.invoke(target, args) |
proxy.invokeSuper(obj, args) |
五、Spring Boot为什么默认用CGLIB?
Spring Boot默认使用CGLIB,原因有以下几点:
- 灵活性:Spring的AOP常用于无接口的类(比如普通的POJO),CGLIB无需接口支持更通用。
- 性能:CGLIB避免反射,执行效率更高,尤其在高并发场景下。
- 集成方便 :Spring早就把CGLIB集成进来了(
spring-core
依赖里包含),无需额外配置。
不过,Spring也支持JDK动态代理。如果目标类实现了接口,可以通过配置(proxyTargetClass=false
)切换到JDK动态代理。
六、怎么记住这些?
你说记不住API,其实没必要死记硬背。我们抓住几个关键点:
- JDK动态代理 :想到"接口"和"反射",API主要是
Proxy.newProxyInstance
和InvocationHandler
。 - CGLIB :想到"继承"和"效率高",API主要是
Enhancer
和MethodInterceptor
,记住methodProxy
是性能关键。 - Spring Boot:默认CGLIB,记"灵活+性能"就够了。
下次回忆时,问自己:
- 有没有接口?有→JDK动态代理,没→CGLIB。
- 性能敏感吗?敏感→CGLIB。
- Spring默认啥?CGLIB!
七、总结
JDK动态代理和CGLIB动态代理各有千秋:
- JDK动态代理是"正统选手",适合有接口的场景,但反射让它稍慢。
- CGLIB是"灵活高手",通过继承和字节码操作(尤其是
methodProxy
)实现高效代理,Spring Boot也因此偏爱它。
希望这篇博客能帮你理清思路,尤其是CGLIB的intercept
参数和它与JDK的区别。下次再遇到动态代理时,不用死记API,只需抓住"接口还是继承""反射还是字节码",就能轻松搞定!