文章目录
- CGLIB介绍
- CGLIB使用示例
- CGLIB核心原理分析
- 代理类分析
- 代理方法分析
- FastClass机制分析
CGLIB介绍
CGLIB(Code Generation Library)是一个开源项目!是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。Hibernate用它来实现PO(Persistent Object 持久化对象)字节码的动态生成。
CGLIB是一个强大的高性能的代码生成包。它广泛的被许多AOP的框架使用,例如Spring AOP为他们提供方法的interception(拦截)。
CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。
除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
CGLIB使用示例
首先引入对应依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
以下是一个简单的CGLIB代理示例:
需要注意的是:
由于是采用的是继承方式,因此final类无法使用CGLIB来进行代理。此外,对于static方法或final方法,由于这些方法无法被重写,所以CGLIB也无法为其提供代理。
/**
* 目标对象,没有实现任何接口
*/
public class UserDaoForCglib {
public void save() {
System.out.println("----cglib代理----已经保存数据!----");
}
//final方法无法被重写,也就无法被代理
public final void saveFinal() {
System.out.println("final方法不可继续重写,所以不能进行代理");
}
//static方法无法被重写,也就无法被代理
public static void saveStatic() {
System.out.println("static方法不可继续重写,所以不能进行代理");
}
}
下面是一个Cglib代理工厂类,创建代理对象核心步骤如下
- 创建Enhancer实例
- 通过setSuperclass方法来设置目标类
- 通过setCallback 方法来设置拦截对象
- create方法生成Target的代理类,并返回代理类的实例
/**
* Cglib子类代理工厂
* 对UserDao在内存中动态构建一个子类对象
*/
public class ProxyFactoryForCglib implements MethodInterceptor {
//维护目标对象
private Object target;
public ProxyFactoryForCglib(Object target) {
this.target = target;
}
//给目标对象创建一个代理对象
public Object getProxyInstance(){
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\code");
//1.工具类
Enhancer en = new Enhancer();
//2.设置父类
en.setSuperclass(target.getClass());
//3.设置回调函数
en.setCallback(this);
//4.创建子类(代理对象)
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("开始事务...");
//执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务...");
return returnValue;
}
}
测试类:
@Test
public void testCglib() {
//目标对象
UserDaoForCglib target = new UserDaoForCglib();
//代理对象
UserDaoForCglib proxy = (UserDaoForCglib) new ProxyFactoryForCglib(target).getProxyInstance();
//执行代理对象的方法
proxy.save();
}
在CGLIB动态代理的过程中,字节码是运行时生成的,通常我们不能直接查看到这些字节码,因为它们是在内存中动态生成并直接加载的。但是,如果你想要分析这个过程,我们可以通过一些工具来打印或保存这些生成的字节码。
CGLIB本身并不提供直接打印字节码到控制台的功能,但是可以使用DebuggingClassWriter
来将生成的字节码保存到文件系统中。然后,我们可以使用一些字节码查看工具来查看这些类的内容。
上面的getProxyInstance
方法中使用了System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\code")
方法将生成的字节码保存到指定的文件路径,此时将生成的字节码采用idea打开即可:
总共会有3个类!按道理说应该只有一个,多出来的两个类怎么回事?
其实多出来的这两个class类就是为CGLIB中重要的fastClass机制而生成的。下面会另外讲解fastClass机制
CGLIB核心原理分析
Cglib的核心原理是在运行时动态生成字节码,以创建实例对象并拦截方法调用。当我们使用Enhancer创建代理对象时,Cglib会动态生成一个新的Java类,该类继承自被代理类,并覆盖被代理类的方法。在覆盖的方法中,Cglib会调用用户定义的MethodInterceptor回调,并将方法调用转发给被代理对象。
代理类分析
CGLIB动态代理在应用时,实际上是通过继承被代理类来创建一个子类,并在子类中覆写方法实现增强。在运行时,CGLIB会使用Java二进制代码生成技术,生成被代理类的子类的字节码,并加载到JVM中。这个过程并不需要被代理类的源代码。
CGLIB代理的原理可以简化为以下几步:
- 生成子类:实现对被代理类的继承,覆写其方法。
- 方法拦截:在子类中覆写的方法里,调用
MethodInterceptor
里的intercept
方法来实现方法的拦截。 - 执行代理方法:通过
MethodProxy
来调用被代理类原有的方法,此时可以在调用前后执行自定义逻辑。
与JDK动态代理相比,CGLIB能够代理普通类,不仅仅是接口。这是因为CGLIB通过直接操作字节码,生成被代理类的子类,因此它不受只能代理接口的限制。
以下是简化后的代理类:UserDaoForCglib$$EnhancerByCGLIB$$5b79f296
类是由CGLIB生成的 HelloService
类的子类。类名中包含了原始类名、CGLIB特有的标识和一串哈希值,以保证类名的唯一性。
public class UserDaoForCglib$$EnhancerByCGLIB$$5b79f296 extends UserDaoForCglib implements Factory {
private boolean CGLIB$BOUND;
private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
private static final Callback[] CGLIB$STATIC_CALLBACKS;
private MethodInterceptor CGLIB$CALLBACK_0;
private static final Method CGLIB$save$0$Method;
private static final MethodProxy CGLIB$save$0$Proxy;
static void CGLIB$STATICHOOK1() {
CGLIB$THREAD_CALLBACKS = new ThreadLocal();
Class var0;
ClassLoader var10000 = (var0 = Class.forName("com.apple.designpattern.objectenhance.proxy3.UserDaoForCglib$$EnhancerByCGLIB$$5b79f296")).getClassLoader();
CGLIB$emptyArgs = new Object[0];
CGLIB$save$0$Proxy = MethodProxy.create(var10000, (CGLIB$save$0$Method = Class.forName("com.apple.designpattern.objectenhance.proxy3.UserDaoForCglib").getDeclaredMethod("save")).getDeclaringClass(), var0, "()V", "save", "CGLIB$save$0");
}
final void CGLIB$save$0() {
super.save();
}
public final void save() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$save$0$Method, CGLIB$emptyArgs, CGLIB$save$0$Proxy);
} else {
super.save();
}
}
private static final void CGLIB$BIND_CALLBACKS(Object var0) {
UserDaoForCglib$$EnhancerByCGLIB$$5b79f296 var1 = (UserDaoForCglib$$EnhancerByCGLIB$$5b79f296)var0;
if (!var1.CGLIB$BOUND) {
var1.CGLIB$BOUND = true;
Object var10000 = CGLIB$THREAD_CALLBACKS.get();
if (var10000 == null) {
var10000 = CGLIB$STATIC_CALLBACKS;
if (var10000 == null) {
return;
}
}
var1.CGLIB$CALLBACK_0 = (MethodInterceptor)((Callback[])var10000)[0];
}
}
static {
CGLIB$STATICHOOK1();
}
}
首先我们可以发现:由于是采用的是继承方式,因此final类无法使用CGLIB来进行代理。此外,对于static方法或final方法,由于这些方法无法被重写,所以CGLIB也无法为其提供代理。
所以我们字节码文件中也不能重写原来的saveFinal
和saveStatic
方法
代理方法分析
重点来看看save
方法:
public final void save() {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$save$0$Method, CGLIB$emptyArgs, CGLIB$save$0$Proxy);
} else {
super.save();
}
}
1、动态代理的回调初始化
首先查找当前对象(this
)中名为CGLIB$CALLBACK_0
的MethodInterceptor
字段。这个字段存储了之前设置的回调接口实例,通常在代理对象生成阶段被初始化。如果CGLIB$CALLBACK_0
为null
,则通过调用CGLIB$BIND_CALLBACKS(this)
试图进行绑定或初始化。这个过程保证了在实际执行代理方法之前,回调接口已被正确设置。
2、方法拦截器的调用处理
随后进行一个判断,如果CGLIB$CALLBACK_0
(也就是MethodInterceptor
的实例)不为null
,意味着我们有方法拦截逻辑需要执行。此时,通过调用拦截器的intercept
方法来处理需要代理的方法(这里为save
方法)的调用。
这个intercept
方法的四个参数意义如下:
- this —— 代表当前代理对象的实例;
- CGLIB$save00Method —— 表示静态变量引用,它直接指向被代理类中的
save
方法的Method
对象。CGLIB 通过 ASM(一种Java字节码操作和分析框架)在类加载时期生成代理类,所以这里使用直接指向save
方法的引用提高了效率; - CGLIB$emptyArgs —— 方法调用时本应传入的参数数组,这里表示
save
方法没有参数; - CGLIB$save00Proxy —— 对应于
save
方法的代理方法引用,其内部逻辑由 CGLIB 生成并包含了原方法的调用。如果需要,可以通过它来直接调用原始save
方法。
3、降级执行逻辑
如果CGLIB$CALLBACK_0
为null
,也就是说没有为save
方法设置拦截逻辑,则直接调用父类的save
方法,这就完成了一个基本的方法拦截逻辑和调用。
所以正常情况我们会调用到var10000.intercept
方法 最终也就是ProxyFactoryForCglib
中的intercept
方法,在这里我们就可以做自己的一些拦截操作,例如日志记录、权限检查、事务处理等等
public class ProxyFactoryForCglib implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("开始事务...");
//执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("提交事务...");
return returnValue;
}
}
FastClass机制分析
CGLIB的FastClass机制是其性能优化的一个重要方面。FastClass机制通过为被代理类和代理类各自生成一个FastClass类来加速方法的调用。FastClass不使用反射来调用被代理类的原始方法,而是采用索引号来直接调用,从而避免了反射调用的性能开销。
UserDaoForCglib$$FastClassByCGLIB$$3c746232.class
给被代理类生成一个FastClass类UserDaoForCglib$$EnhancerByCGLIB$$5b79f296$$FastClassByCGLIB$$f8bb03ec.class
给代理类生成一个FastClass类
FastClass机制背后的核心是一个巨大的switch语句,每一个case对应被代理类中的一个方法。调用方法时只需传入方法的索引和参数,FastClass即可直接定位并调用目标方法。
当我们调用intercept
方法时,实际上是通过FastClass机制找到方法的索引,然后通过索引快速调用被代理的方法。
public class UserDaoForCglib$$FastClassByCGLIB$$3c746232 extends FastClass {
public UserDaoForCglib(Class classToProxy) {
super(classToProxy);
}
public int getIndex(String signature) {
// 根据方法签名查找方法的索引
}
public Object invoke(int index, Object obj, Object[] args) {
// 根据索引直接执行对应的方法
UserDaoForCglib instance = (UserDaoForCglib) obj;
switch(index) {
case 0:
instance.test();
return null;
default:
throw new IllegalArgumentException();
}
}
}
FastClass主要完成了两个任务:(理解成MySQL通过索引快速定位查询数据)
- 将方法的调用转换成索引的调用,这个索引是在FastClass生成时就确定好的。
- 通过索引快速定位并直接调用目标方法,跳过反射调用的开销。
FastClass机制大大提升了CGLIB动态代理的调用效率,让动态代理的成本降低,这也是CGLIB在性能上通常优于JDK动态代理的一个重要原因。