JDK 动态代理和 CGLib
介绍
JDK 动态代理
Java 标准库提供了动态代理功能,允许程序在运行期动态创建指定接口的实例。
CGLib 动态代理
使用 ASM 框架,加载代理对象的 Class 文件,通过修改其字节码生成子类。
适用场景
- JDK 动态代理适用于实现接口的类,对未实现接口的类无能为力。
- CGLib 不要求类实现接口,但对 final 方法无能为力。
性能比较
- 在 JDK 8 以前,CGLib 性能更好
- 从 JDK 8 开始,JDK 动态代理性能更好
根据 README.md 的提醒,cglib 已经不再维护,且在较新版本的 JDK 尤其是 JDK 17+ 中表现不佳,官方推荐可以考虑迁移到 ByteBuddy。在如今越来越多的项目迁移到 JDK 17 的背景下,值得注意。
使用
代理对象的类和接口
代理对象的类和实现的接口:
HelloService.java
1
2
3public interface HelloService {
void sayHello(String name);
}HelloService.java
1
2
3
4
5
6
7
8
9public class HelloServiceImpl implements HelloService {
public void sayHello(String name) {
if (name == null) {
throw new IllegalArgumentException("name can not be null");
}
System.out.println("Hello " + name);
}
}
JDK 动态代理示例
- 自定义 InvocationHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class UserServiceInvocationHandler implements InvocationHandler {
private Object target;
public UserServiceInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
System.out.println("do sth. before invocation");
Object ret = method.invoke(target, args);
System.out.println("do sth. after invocation");
return ret;
} catch (Exception e) {
System.out.println("do sth. when exception occurs");
throw e;
} finally {
System.out.println("do sth. finally");
}
}
} - 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class JdkProxyTest {
public static void main(String[] args) {
HelloService target = new HelloServiceImpl();
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
UserServiceInvocationHandler invocationHandler = new UserServiceInvocationHandler(target);
HelloService proxy = (HelloService) Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
proxy.sayHello("Tom");
System.out.println("=================");
proxy.sayHello(null);
}
} - 结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20do sth. before invocation
Hello Tom
do sth. after invocation
do sth. finally
=================
do sth. before invocation
do sth. when exception occurs
do sth. finally
Exception in thread "main" java.lang.reflect.UndeclaredThrowableException
at com.sun.proxy.$Proxy0.sayHello(Unknown Source)
at com.moralok.proxy.jdk.JdkProxyTest.main(JdkProxyTest.java:19)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.moralok.proxy.jdk.UserServiceInvocationHandler.invoke(UserServiceInvocationHandler.java:18)
... 2 more
Caused by: java.lang.IllegalArgumentException: name can not be null
at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)
CGLib 动态代理示例
- 引入依赖
1
2
3
4
5
6
7<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies> - 自定义 MethodInterceptor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class UserServiceMethodInterceptor implements MethodInterceptor {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
try {
System.out.println("do sth. before invocation");
Object ret = proxy.invokeSuper(obj, args);
System.out.println("do sth. after invocation");
return ret;
} catch (Exception e) {
System.out.println("do sth. when exception occurs");
throw e;
} finally {
System.out.println("do sth. finally");
}
}
} - 测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class CglibTest {
public static void main(String[] args) {
UserServiceMethodInterceptor methodInterceptor = new UserServiceMethodInterceptor();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloServiceImpl.class);
enhancer.setCallback(methodInterceptor);
HelloService proxy = (HelloService) enhancer.create();
proxy.sayHello("Tom");
System.out.println("=================");
proxy.sayHello(null);
}
} - 结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16do sth. before invocation
Hello Tom
do sth. after invocation
do sth. finally
=================
do sth. before invocation
do sth. when exception occurs
do sth. finally
Exception in thread "main" java.lang.IllegalArgumentException: name can not be null
at com.moralok.proxy.HelloServiceImpl.sayHello(HelloServiceImpl.java:8)
at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.CGLIB$sayHello$0(<generated>)
at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.invoke(<generated>)
at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
at com.moralok.proxy.cglib.UserServiceMethodInterceptor.intercept(UserServiceMethodInterceptor.java:14)
at com.moralok.proxy.HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.sayHello(<generated>)
at com.moralok.proxy.cglib.CglibTest.main(CglibTest.java:18)
查看 JDK 生成的代理类
使用以下语句,将在工作目录下生成代理类的 Class 文件。
1 | System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); |
1 | public final class $Proxy0 extends Proxy implements HelloService { |
查看 CGLib 生成的子类
使用以下语句,将 CGLib 生成的子类的 Class 文件输出到指定目录,会发现出现了 3 个 Class 文件。
1 | System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "C:\\Users\\username\\Class"); |
- HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31.class,代理类
- HelloServiceImpl$$FastClassByCGLIB$$a5654167.class,被代理类的 FastClass
- HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511.class,代理类的 FastClass
代理类定义
继承了被代理类。
1 | public class HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31 extends HelloServiceImpl implements Factory { |
静态代码块
1 | static { |
MethodProxy 稍后再做介绍。
构造器方法
构造器方法内,调用了绑定回调(Callbacks)方法。
1 | public HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31() { |
生成的代理方法
CGLib 会为每一个代理方法生成两个对应的方法,一个直接调用父类方法,一个则调用回调(拦截器)的 intercept 方法。
1 | final void CGLIB$sayHello$0(String var1) { |
CGLib 通过继承实现动态代理的过程,在查看生成的子类的 Class 后,是非常容易理解的。拦截器的参数有代理对象、Method、方法参数和 MethodProxy 对象。
分析 MethodProxy
如何在拦截器中调用被代理的方法呢?就是通过 MethodProxy 实现的。
创建 MethodProxy
MethodProxy 是 CGLib 为每一个代理方法创建的方法代理,当调用拦截的方法时,它被传递给 MethodInterceptor 对象的 intercept 方法。它可以用于调用原始方法,或对同一类型的不同对象调用相同方法。
1 | CGLIB$sayHello$0$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "sayHello", "CGLIB$sayHello$0"); |
CreateInfo 静态内部类,保存被代理类和代理类以及其他一些信息。
1 | private static class CreateInfo |
FastClass 和方法索引对
调用原始方法 invokeSuper
MethodProxy 通过 invokeSuper 调用原始方法(父类方法)。
1 | // invoke 方法的代码相似 |
生成 FastClass 信息
1 | private void init() |
FastClass 信息
- f1 是被代理类的 FastClass 对象,i1 是
CGLIB$sayHello$0
方法在生成的 FastClass 中的索引。 - f2 是代理类的 FastClass 对象,i2 是
sayHello
方法在生成的 FastClass 中的索引。
invoke 方法根据传入的方法索引,快速定位要调用对象 obj 的哪个方法。
CGLib 完全有能力获得
CGLIB$sayHello$0
的 Method 对象,通过反射实现调用,这样处理逻辑更加清楚。但是早期 Java 反射的性能并不好,通过 FastClass 机制避免使用反射从而提升了性能。
1 | private static class FastClassInfo |
FastClass 的 invoke 方法
以代理类的 FastClass HelloServiceImpl$$EnhancerByCGLIB$$c51b2c31$$FastClassByCGLIB$$c068b511
为例,当传入的方法索引为 16 时,就会调用 CGLIB$sayHello$0
方法。
- 获取代理对象
- 根据传入的方法索引,调用
1 | public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException { |
获取方法索引
怎么知道方法的索引呢?在初始化 FastClass 信息时,不仅生成了 FastClass,还通过 getIndex 获取方法的索引。
在 JDK 7 之后,switch 不仅可以支持 int、enum,还能支持 String,CGLib 这样实现是出于兼容性的考虑还是说有什么性能提升?
1 | public int getIndex(Signature var1) { |
总结和思考
两者在使用上是相仿的。
- 对于两者的源码,读得不多。有时候会感慨,看这么多年前的代码,还是感觉吃力。有时候想,如果不好好看源码,心里不踏实;如果花很多时间理清楚了,但是发现更多只是知道了一些细节,于整体理解的提升不大,又会感觉不值得。
- 但也提醒自己,不要太在意,用得本就不多,涉及源码的机会更是没有,如果方方面面都要细究,人生太短,智商不够,等涉足相关问题再回头研究。
- 基础的用法和概念应该了解,不然看到 Spring AOP 源码时,分不清 Spring 封装的边界在哪里。
借着梳理 Spring 的机会回头再看,又感觉轻松不少。