JDK 和 CGLib 实现动态代理和区别
在日常的开发中,Spring AOP 是一个非常常用的功能。谈到 AOP,自然离不开动态代理。
那么,基于 JDK 和 CGLib 如何实现动态代理,他们之间的区别和适用场景是什么呢?接下来,我们一起来探讨一下这个问题。
JDK 如何实现动态代理?
话不多说,我们直接对照着代码来查看。
代码示例
Hello 接口
public interface HelloInterface {
/**
* 代理的目标方法
*/
void sayHello();
/**
* 未被代理处理的方法
*/
void noProxyMethod();
}
Hello 实现类
public class HelloImpl implements HelloInterface {
@Override
public void sayHello() {
System.out.println("proxyMethod:sayHello");
}
@Override
public void noProxyMethod() {
System.out.println("noProxyMethod");
}
}
MyInvocationHandler 实现 InvocationHandler 接口类
public class MyInvocationHandler implements InvocationHandler {
/**
* 目标对象
*/
private Object target;
/**
* 构造方法
*
* @param target
*/
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if ("sayHello".equals(methodName)) {
// 比方说,mybaitis 中的 PooledConnection 利用 jdk 动态代理重新实现了 close 方法
System.out.println("change method");
return null;
}
System.out.println("invoke method");
Object result = method.invoke(target, args);
return result;
}
}
动态代理神奇的地方就是:
- 代理对象是在程序运行时产生的,而不是编译期;
- 对代理对象的所有接口方法调用都会转发到
InvocationHandler.invoke()
方法,在invoke()
方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等。
⚠️注意:从 Object 中继承的方法,JDK Proxy 会把hashCode()、equals()、toString()这三个非接口方法转发给 InvocationHandler,其余的 Object 方法则不会转发。详见 JDK Proxy官方文档。
代码测试
public class MyDynamicProxyTest {
public static void main(String[] args) {
HelloInterface hello = new HelloImpl();
MyInvocationHandler handler = new MyInvocationHandler(hello);
// 构造代码实例
HelloInterface proxyInstance = (HelloInterface) Proxy.newProxyInstance(
HelloImpl.class.getClassLoader(),
HelloImpl.class.getInterfaces(),
handler);
// 代理调用方法
proxyInstance.sayHello();
proxyInstance.noProxyMethod();
}
}
打印的日志信息如下:
关键要点
结合上面的演示,我们小结一下 JDK 动态代理的实现,包括三个步骤:
-
1.定义一个接口
比如上面的 HelloInterface,Jdk 的动态代理是基于接口,这就是代理接口。
-
2.编写接口实现类
比如上面的 HelloImpl,这个就是目标对象,也就是被代理的对象类。
-
3.编写一个实现 InvocationHandler 接口的类,代理类的方法调用会被转发到该类的 invoke() 方法。
比如上面的 MyInvocationHandler。
CGLib 如何实现动态代理?
代码示例
Hello 类
无需定义和实现接口。
public class Hello {
public String sayHello(String name) {
System.out.println("Hello," + name);
return "Hello," + name;
}
}
CglibMethodInterceptor 实现 MethodInterceptor
/**
* 实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
*/
public class CglibMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("intercept param is " + Arrays.toString(args));
System.out.println("before===============" + method);
// 这里可以实现增强的逻辑处理s
Object result = methodProxy.invokeSuper(obj, args);
// 这里可以实现增强的逻辑处理
System.out.println("after===============" + method);
return result;
}
}
⚠️注意:对于从Object中继承的方法,CGLIB代理也会进行代理,如
hashCode()
、equals()
、toString()
等,但是getClass()
、wait()
等方法不会(因为其他方法是 final,无法被代理),CGLIB 无法代理她们。
pom 依赖
<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
</dependencies>
代码测试
public class CglibTest {
/**
* 在需要使用 Hello 的时候,通过CGLIB动态代理获取代理对象
*/
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Hello.class);
enhancer.setCallback(new CglibMethodInterceptor());
// 给目标对象创建一个代理对象
Hello hello = (Hello) enhancer.create();
hello.sayHello("Alan");
}
打印的日志如下:
关键要点
结合上面的演示,我们小结一下 CGLIB 动态代理的实现:
- 1.实现一个MethodInterceptor,方法调用被转发到该类的 intercept() 方法。
- 2.使用 Enhancer 获取代理对象,并调用对应的方法。
JDK Vs CgLib
Java 1.3 后,提供了动态代理技术,允许我们开发者在运行期创建接口的代理实例,后来这项技术被用到了很多地方(比如 Spring AOP)。
JDK 动态代理主要对应到 java.lang.reflect 包下边的两个类:Proxy 和 InvocationHandler。
其中 InvocationHandler 是一个接口,可以通过实现该接口定义横切逻辑。
举个例子,在方法执行前后打印的日志(这里只是为了说明,实际应用一般不会只是简单的打印日志,一般用于日志、安全、事务等场景),并通过「反射机制」调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。
-
JDK 动态代理有一个限制:它只能为接口创建代理实例。
对于没有通过接口定义业务方法的类,如何创建动态代理实例呢?答案就是 CGLib。
-
CGLIB(Code Generation Library))是一个底层基于 ASM 的字节码生成库,它允许我们在「运行时」修改和动态生成字节码。
CGLIB 通过继承方式实现代理,在子类中采用方法拦截的方式拦截所有父类方法的调用并顺势织入横切逻辑。
JDK 和 CGLib 动态代理区别
1. JDK 动态代理实现原理
- 通过实现 InvocationHandler 接口创建自己的调用处理器
- 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 创建动态代理
- 通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型
- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数传入
JDK 动态代理是面向接口的代理模式,如果被代理目标没有接口则无能为力。
例如,Spring 通过 Java 的反射机制生产被代理接口的新的匿名实现类,重写了 AOP 的增强方法。
2. CGLib 动态代理原理
利用 ASM 开源包,对代理对象类的 class 文件加载进来,通过修改其字节码生成子类来处理。
3. 两者对比
- JDK 动态代理是面向接口的;
- CGLib 动态代理是通过字节码底层继承代理类来实现,如果被代理类被 final 关键字所修饰,则无法被代理。
4.适用场景
-
如果被代理的对象是个实现了接口的实现类,那么可以使用 JDK 动态代理。
例如,Spring 会使用 JDK 动态代理来完成操作(Spirng 默认采用)
-
如果被代理的对象没有实现接口,只有实现类,那么只能使用 CGLib 实现动态代理(JDK 不支持)。
例如,被代理对象是没有接口的实现类,Spring 强制使用 CGLib 实现的动态代理。
性能对比
网上有人对于不通版本的 jdk 进行了测试,经过多次试验,测试结果大致如下:
- 在 JDK 1.6 和 1.7 时,JDK 动态代理的速度要比 CGLib 要慢,但是并没有某些书上写的10倍差距那么夸张。
- 在 JDK 1.8 时,JDK 动态代理的速度比 CGLib 快很多。
[idea] 很多时候,性能差异不一定是我们选择某种方式的绝对因素,我们更应该去考虑该技术适用的场景。
例如,我们应用中绝大多数的性能差异可能主要在集中在磁盘 I/O,网络带宽等因素,动态代理这点性能差异可能只是占了非常小的比例。
Reference
- https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/Proxy.html
- https://www.cnblogs.com/CarpenterLee/p/8241042.html
- https://blog.csdn.net/shallynever/article/details/103351299
END