动态代理(二)—— CGLIB代理原理

前篇文章动态代理(一)——JDK中的动态代理中详细介绍了JDK动态代理的Demo实现,api介绍,原理详解。这篇文章继续讨论Java中的动态代理,并提及了Java中动态代理的几种实现方式。这里继续介绍CGLIB代理方式。

CGLIB动态代理在AOP、RPC中都有所使用,是Java体系中至关重要的一块内容。本篇文章的主要目标:

  • 掌握使用CGLIB生成代理类
  • 深入理解CGLIB的代理原理

从以上目标出发,本篇文章主要从以下几个方面逐步深入探索CGLIB:

  • CGLIB的使用Demo
  • CGLIB重要API介绍
  • CGLIB代理原理
  • 总结

一.CGLIB的使用Demo

使用CGLIB的大致分为四步骤:

  1. 创建被代理对象
  2. 创建方法拦截器
  3. 创建代理对象
  4. 调用代理对象

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等技术提供了实现基础。

在反编译看代理对象的源代码之前,先看下代理调用的过程图:

从图中可以看出:

  1. 客户端调用代理对象的被代理方法
  2. 代理对象将调用委派给方法拦截器统一接口intercept
  3. 方法拦截器中执行前置操作,然后调用方法代理的统一接口invokeSuper
  4. 方法代理的invokeSuper初始化代理对象的和被代理对象的fastClass
  5. 初始化后,再调用代理对象的fastClass
  6. 代理对象的fastClass能够fast的调用代理的代理对象
  7. 代理对象再调用被代理对象的被代理方法
  8. 调用栈弹出,到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机制:将类的方法信息解析出来,然后为其建立索引。调用的时候,只要传索引,就能找到相应的方法进行调用。

  1. 为所有的方法建立索引
  2. 调用前先根据方法信息寻找到索引
  3. 调用时根据索匹配相应的方法进行直接调用

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中生成的代理类和相应的代理类:

  1. EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce$$FastClassByCGLIB$$1b37f797.class
  2. EchoServiceImpl$$EnhancerByCGLIB$$e77dd5ce.class
  3. 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

猜你喜欢

转载自www.cnblogs.com/lxyit/p/9328294.html