动态代理—— CGLIB代理原理
Posted lxyit
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态代理—— CGLIB代理原理相关的知识,希望对你有一定的参考价值。
前篇文章动态代理(一)——JDK中的动态代理中详细介绍了JDK动态代理的Demo实现,api介绍,原理详解。这篇文章继续讨论Java中的动态代理,并提及了Java中动态代理的几种实现方式。这里继续介绍CGLIB代理方式。
CGLIB动态代理在AOP、RPC中都有所使用,是Java体系中至关重要的一块内容。本篇文章的主要目标:
- 掌握使用CGLIB生成代理类
- 深入理解CGLIB的代理原理
从以上目标出发,本篇文章主要从以下几个方面逐步深入探索CGLIB:
- CGLIB的使用Demo
- CGLIB重要API介绍
- CGLIB代理原理
- 总结
一.CGLIB的使用Demo
使用CGLIB的大致分为四步骤:
- 创建被代理对象
- 创建方法拦截器
- 创建代理对象
- 调用代理对象
1.创建被代理对象
public class EchoServiceImpl implements EchoService {
public void echo(String message) {
System.out.println(message);
}
public void print(String message) {
System.out.println(message);
}
public int test() {
return 1;
}
public final void finalTest() {
System.out.println("I am final method.");
}
}
2.创建方法拦截器
public class EchoServiceInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("before invoking!");
Object object = methodProxy.invokeSuper(o, objects);
System.out.println("after invoking!");
return object;
}
}
接口MethodInterceptor是CGLIB库提供,用于应用开发者根据自己的业务逻辑进行扩展实现。
3.创建代理对象
//Enhancer是生成代理类的工厂
Enhancer enhancer = new Enhancer();
//设置代理的超类,即被代理对象
enhancer.setSuperclass(EchoServiceImpl.class);
//设置拦截方法
enhancer.setCallback(new EchoServiceInterceptor());
//生成代理对象
EchoService echoService = (EchoService) enhancer.create();
4.调用代理对象
echoService.echo("test");
执行结果:
before invoking!
test
after invoking!
二.CGLIB重要API介绍
CGLIB库的体积很小,但是学习难度确非常高,毕竟涉及到bytecode。所以该篇文章后续关于原理介绍,只涉及代理原理方面,关于如何生成代理对象的方面,由于个人能力所及,不敢妄加说明。
下面是CGLIB的包结构,每个包都是负责一个模块功能,定义非常明确,负责单一的功能职责:
net.sf.cglib.core
低级别的字节码操作的类,它们直接与ASM相关net.sf.cglib.transform
在运行或者构建时转换类文件的一些类net.sf.cglib.proxy
创建代理和方法拦截器定义的类net.sf.cglib.reflect
实现快速反射的一些基础类net.sf.cglib.util
用于集合排序的一些工具类net.sf.cglib.beans
与JavaBean相关的类
虽然cglib包含了如此多的功能模块,但是对于使用者,我们并不需要关注如此多的细节,只需要掌握几个重要的接口:
在看完上面的Demo,应该对Enhancer有一定了解。Enhancer字面义即增强,也正如其表述,Enhancer就是用来创建代理对象的接口。其中create方法可以生成代理对象,实际就是工厂模式。
在生成对象前,需要做关于代理方面的配置:
- 配置被代理对象(目标),即setSuperClass设置超类型,该superClass即Enhancer中持有的Class对象;
- 配置统一拦截方法(中间人),即setCallBack设置回调接口,对应上图的CallBack。AOP的实现使用methodInterpretor型CallBack;
- 可选性的配置拦截过滤器(核验流程),即setCallBackFilter,对应上图的CallBackFiler;
Enhancer的create api提供了生成代理对象的。以上即在编写cglib动态代理过程中使用的几个重要api。虽然字节码技术是非常晦涩深奥,但是cglib以简单易用的api使字节码增强技术变得非常容易上手。
通过以上的demo示例和几个重要api的介绍应该都能掌握使用cglib库生成代理类。下面依然通过反编译的方式继续深入cglib的动态代理的调用原理。
三.CGLIB代理原理
1.整体架构与调用过程概览
在详细查看被代理对象的原理之前,先了解下cglib的整体架构图:
从图中可以看出,cglib在字节码层面的操作技术主要依赖ASM提供的能力。在上节中提到的net.sf.cglib.core包,正是与ASM相关。CGLIB上层直接面向应用层,将深奥晦涩的字节码技术包装成应用易用能理解的api,为aop,dynaminc proxy等技术提供了实现基础。
在反编译看代理对象的源代码之前,先看下代理调用的过程图:
从图中可以看出:
- 客户端调用代理对象的被代理方法
- 代理对象将调用委派给方法拦截器统一接口intercept
- 方法拦截器中执行前置操作,然后调用方法代理的统一接口invokeSuper
- 方法代理的invokeSuper初始化代理对象的和被代理对象的fastClass
- 初始化后,再调用代理对象的fastClass
- 代理对象的fastClass能够fast的调用代理的代理对象
- 代理对象再调用被代理对象的被代理方法
- 调用栈弹出,到intercept中再执行后置操作,方法调用结束
通过以上的过程再来看下它们之间的UML:
在Enhancer中的配置的被代理对象、统一回调的最终都被聚合到生成的代理对象中。(工厂模式,零件组装成产品)
代理对象聚合同一方法回调、继承被代理对象,聚合方法代理的。
2.反编译代理类字节码
CGLIB提供生成代理类的class文件的配置项。在CGLIB中提供了DebuggingClassWriter类用于将字节码的byte字节写入class文件中。
public byte[] toByteArray() {
return (byte[]) java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction() {
public Object run() {
// 获取代理类的字节内容
byte[] b = ((ClassWriter) DebuggingClassWriter.super.cv).toByteArray();
if (debugLocation != null) {
// 转换生成的class文件路径分隔符
String dirs = className.replace(‘.‘, File.separatorChar);
try {
// 创建class文件
new File(debugLocation + File.separatorChar + dirs).getParentFile().mkdirs();
File file = new File(new File(debugLocation), dirs + ".class");
OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
try {
// 将字节内容写入class文件
out.write(b);
} finally {
out.close();
}
if (traceCtor != null) {
file = new File(new File(debugLocation), dirs + ".asm");
out = new BufferedOutputStream(new FileOutputStream(file));
try {
ClassReader cr = new ClassReader(b);
PrintWriter pw = new PrintWriter(new OutputStreamWriter(out));
ClassVisitor tcv = (ClassVisitor)traceCtor.newInstance(new Object[]{null, pw});
cr.accept(tcv, 0);
pw.flush();
} finally {
out.close();
}
}
} catch (Exception e) {
throw new CodeGenerationException(e);
}
}
return b;
}
});
}
从以上可以看出只要配置文件的生成路径变量debugLocation即可,再来看下该变量初始化赋值情况
static {
// 从System中取出属性DEBUG_LOCATION_PROPERTY赋值给文件class文件生成路径变量
debugLocation = System.getProperty(DEBUG_LOCATION_PROPERTY);
if (debugLocation != null) {
System.err.println("CGLIB debugging enabled, writing to ‘" + debugLocation + "‘");
try {
Class clazz = Class.forName("org.objectweb.asm.util.TraceClassVisitor");
traceCtor = clazz.getConstructor(new Class[]{ClassVisitor.class, PrintWriter.class});
} catch (Throwable ignore) {
}
}
}
可以看出只要应用启动时
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,
"/Users/lixinyou/Documents/code-space/java/java-base/java-proxy/target/proxy/impl");
设置DebuggingClassWriter.DEBUG_LOCATION_PROPERTY属性,在运行时CGLIB便会生成代理类的class。
这种方式与JDK的动态代理中的class文件生成方式一致。这种用法,在日常应用开发中也可借鉴,利用应用启动参数的不同,可以在运行时改变行为,代码具有强扩展性。
3.fastClass机制
在看代理机制源码之前,做好一切准备。再来了解下CGLIB中关于实现快速调用的fastClass机制。
在JDK的动态代理中使用反射调用目标对象,在CGLIB中为了更好的提升性能,采用fastClass机制。
FastClass机制:将类的方法信息解析出来,然后为其建立索引。调用的时候,只要传索引,就能找到相应的方法进行调用。
- 为所有的方法建立索引
- 调用前先根据方法信息寻找到索引
- 调用时根据索匹配相应的方法进行直接调用
CGLIB在字节码层面将方法和索引的对应关系建立,避免了反射调用:
public int getIndex(Signature var1) {
String var10000 = var1.toString();
switch(var10000.hashCode()) {
case -2055565910:
if (var10000.equals("CGLIB$SET_THREAD_CALLBACKS([Lnet/sf/cglib/proxy/Callback;)V")) {
return 11;
}
break;
case -1980342926:
if (var10000.equals("print(Ljava/lang/String;)V")) {
return 6;
}
break;
case -1860420502:
if (var10000.equals("CGLIB$clone$7()Ljava/lang/Object;")) {
return 24;
}
break;
case -1725733088:
if (var10000.equals("getClass()Ljava/lang/Class;")) {
return 29;
}
break;
}
return -1;
}
上述获取方法的索引,下述代码再根据索引进行调用:
public Object invoke(int index, Object var2, Object[] var3) throws InvocationTargetException {
e77dd5ce var10000 = (e77dd5ce)var2;
try {
switch(index) {
case 0:
return new Boolean(var10000.equals(var3[0]));
case 1:
return var10000.toString();
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
4.运行时的代理类
CGLIB运行时,实际会生成三个class:
- 代理类
- 代理类对应的fastClass
- 被代理类的fastClass
如上述Demo中生成的代理类和相应的代理类:
- EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce$$FastClassByCGLIB$$1b37f797.class
- EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce.class
- EchoServiceImpl$$FastClassByCGLIB$$44f86581.class
下面就看下生成的类的代码片段,理解下CGLIB的运行时代理原理。
首先看下生成的代理类
public class EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce extends EchoServiceImpl implements Factory {
代理类是继承自被代理类,这里与JDK的不同是,JDK是实现了接口。
下面的即是对echo方法的代理方法:
public final void echo(String var1) {
// 获取方法拦截器
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (this.CGLIB$CALLBACK_0 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) { // 如果不为空,则调用其统一拦截
var10000.intercept(this, CGLIB$echo$2$Method, new Object[]{var1}, CGLIB$echo$2$Proxy);
} else { // 如果为空,则直接调用父类即被代理类的方法
super.echo(var1);
}
}
最为让人关注的是intercept方法调用时的参数MethodProxy:CGLIB$echo$2$Proxy
static {
CGLIB$STATICHOOK1();
}
static void CGLIB$STATICHOOK1() {
CGLIB$echo$2$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/String;)V", "echo", "CGLIB$echo$2");
}
在代理类被加载时,执行静态方法CGLIB$STATICHOOK1(),创建了echo方法对应的方法代理。
public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
MethodProxy proxy = new MethodProxy();
proxy.sig1 = new Signature(name1, desc);
proxy.sig2 = new Signature(name2, desc);
proxy.createInfo = new CreateInfo(c1, c2);
return proxy;
}
这里使用了工厂模式,在创建MethodProxy时,为期成员CreateInfo赋值。c1代表被代理类,c2代表代理类。desc代理方法和被代理方法的参数信息,name1是被代理方法名,name2是代理方法名。
下面再看来下intercepte方法中调用的methodProxy的invokeSuper方法:
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
try {
// 先进行初始化
init();
// 获取fastClassInfo对象
FastClassInfo fci = fastClassInfo;
// 获取代理类对应的fastClass对象并按索引调用
return fci.f2.invoke(fci.i2, obj, args);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
先看下初始化中所执行的逻辑:
private void init()
{
/*
* Using a volatile invariant allows us to initialize the FastClass and
* method index pairs atomically.
*
* Double-checked locking is safe with volatile in Java 5. Before 1.5 this
* code could allow fastClassInfo to be instantiated more than once, which
* appears to be benign.
*/
if (fastClassInfo == null)
{
synchronized (initLock)
{
if (fastClassInfo == null)
{
CreateInfo ci = createInfo;
FastClassInfo fci = new FastClassInfo();
// 获取被代理类
fci.f1 = helper(ci, ci.c1);
// 获取代理类
fci.f2 = helper(ci, ci.c2);
// 获取被代理类的被代理方法的索引
fci.i1 = fci.f1.getIndex(sig1);
// 获取代理类的代理方法的索引
fci.i2 = fci.f2.getIndex(sig2);
fastClassInfo = fci;
createInfo = null;
}
}
}
}
这里使用了单例模式,fastClassInfo对象是单例。所以初始化方法只会在第一次调用代理方法的时候,才响应的进行对其初始化。
初始化后,就将代理类和代理方法的索引获取到了,然后再按照索引直接对代理方法进行调用:
public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
e77dd5ce var10000 = (e77dd5ce)var2;
int var10001 = var1;
try {
switch(var10001) {
case 19:
var10000.CGLIB$echo$2((String)var3[0]);
return null;
}
} catch (Throwable var4) {
throw new InvocationTargetException(var4);
}
throw new IllegalArgumentException("Cannot find matching method/constructor");
}
上述考虑到篇幅和简洁的原因,这里只摘取了case:19的代码片段。
通过索引直接调用代理类代理方法:CGLIB$echo$2:
final void CGLIB$echo$2(String var1) {
super.echo(var1);
}
在该方法中再调用被代理类(继承了被代理类)的被代理方法。
至此,CGLIB的代理调用原理就是以上的内容。
四.总结
CGLIB是一种字节码增强库,利用其提供的字节码技术可以实现动态代理。其底层依赖ASM字节码技术。
CGLIB的动态代理与JDK动态代理的不同点:
- JDK动态代理必须需要接口,JDK代理是基于接口进行动态代理。CGLIB中既支持对接口的代理,也支持对对象的代理。
- CGLIB动态代理使用fastClass机制实现快速调用被代理类,JDK中使用了反射方式调用被代理。所以CGLIB的动态代理的方式性能上更有优势。
- CGLIB额外对来源于Object中的finalize和clone方法也做了拦截代理,JDK只为了equals、hashCode、toString进行代理
注:JDK中生成的代理类已经静态解析了方法对象作为代理类的静态变量,类似做缓存,从而部分解决反射的性能问题。
CGLIB的动态代理与JDK动态代理的相同点:
- 都具有统一接口,JDK动态代理中中间统一接口是InvocationHandler,CGLIB中是MethodInteceptor。
- 生成的代理类和其中的方法都是final
参考
neoremind/dynamic-proxy
Are there alternatives to cglib
Spring AOP 实现原理与 CGLIB 应用
深入浅出CGlib-打造无入侵的类代理
CGLib: The Missing Manual
Create Proxies Dynamically Using CGLIB Library
以上是关于动态代理—— CGLIB代理原理的主要内容,如果未能解决你的问题,请参考以下文章