zoukankan      html  css  js  c++  java
  • 动态代理(二)—— 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

  • 相关阅读:
    Python单元测试unittest加载方式之二:加载测试套件
    Python单元测试unittest加载方式之二:加载测试套件
    Python单元测试unittest加载方式之一:unittestmain()启动单元测试模块
    和优秀的人相处
    耐心和恒心
    ubuntu下FTP文件目录共享
    一个不错的编译调试方法
    qt 程序异常崩溃
    minigui SetTimer(hwnd, id, speed)
    minigui 按钮点击弹窗外部 弹窗消失功能 WS_EX_TRANSPARENT
  • 原文地址:https://www.cnblogs.com/lxyit/p/9328294.html
Copyright © 2011-2022 走看看