• 面试造火箭系列,栽在了cglib和jdk动态代理


    “喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗”。声音是从电话那头传来的

    “是的,你好”。小帅暗喜,大厂终于找上我了。

    “下面我们来进行一下电话面试吧,请先自我介绍一下吧”

    “balabalabla...”小帅把之前的经历大概描述了一下

    “嗯,经历很丰富呀,接下来咱们来聊聊技术吧,请问cglib和jdk动态代理的区别是什么呢?”

    “额(⊙o⊙)…”,张小帅蒙了,场面一度尴尬。

    ......

    面试的事情就发生在刚才,由于第一题就栽了,后面面试官的问题,小帅基本啥信心都没了,此时小帅心情久久不能平静,“这就是大厂的面试么?”,小帅喃喃自语,他万万没想到的是第一题就栽了。

    屏幕前的小伙伴是否心中有数呢?接下来,就跟着老猫好好温习一下吧。


    聊起动态代理,咱们还是从代理模式先着手来看看吧。

    代理模式

    关于代理模式,查阅比较专业的资料是这么定义的:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。

    主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

    上述概念看起来比较模糊,举些日常的例子,比方说火车票是个目标对象,咱们要去买,那么我们不一定非得去火车站才能买到,其实在很多代理点也能买到。再比如猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。

    静态代理模式

    代码演示
    下面咱们通过买火车票的案例演示一下代理模式,具体代码如下:
    抽象接口:

    /**
     * @Author: 老猫
     * @Description: 票
     */
    public interface Ticket {
        void getTicket();
    }
    

    火车站实现抽象接口具备买票功能

    /**
     * @Author: 老猫
     * @Description: 火车站
     */
    public class RailwayStation implements Ticket {
    
        @Override
        public void getTicket() {
            System.out.println("买了张火车票");
        }
    }
    

    火车站代理类实现抽象接口具备买票功能

    /**
     * @Author: 老猫
     * @Description: 代理类
     * @Date: 2021/12/22 5:35 下午
     */
    public class RailwayAgencyProxy implements Ticket{
        private RailwayStation railwayStation;
    
        public RailwayAgencyProxy(RailwayStation railwayStation) {
            this.railwayStation = railwayStation;
        }
    
        @Override
        public void getTicket() {
            railwayStation.getTicket();
        }
    }
    

    上述其实就是静态代理模式。

    优点:

    1. 可以做到在符合开闭原则的情况下对目标对象进行功能扩展。
    2. 职责非常清晰,一目了然。

    缺点:

    1. 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
    2. 实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
    3. 代码层面来看,如果接口发生改变,代理类也会发生变更。
    动态代理

    有了上面的基础,咱们正式聊聊动态代理。
    上面的例子其实我们不难发现,每个代理类只能够实现一个接口服务。那么如果当我们的软件工程中出现多个适用代理模式的业务类型时那么咱们就会创建多个代理类,那么我们如何去解决这个问题呢?其实我们的动态代理就应运而生了。

    很显然动态代理类的字节码在程序运行时由Java反射机制动态生成,无需我们手工编写它的源代码。
    那咱们基于上述的案例来先看看JDK动态代理类

    JDK动态代理

    直接看一下JDK动态代理的使用,如下代码块

    public class JDKDynamicProxy implements InvocationHandler {
    
        //被代理的对象
        private Object object;
    
        public JDKDynamicProxy(Object object) {
            this.object = object;
        }
    
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Object result = method.invoke(object,args);
            return result;
        }
    
        //生成代理类
        public Object createProxyObj(){
            return Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),this);
        }
    }
    

    静态代理,动态代理调用如下:

    public class TestProxy {
        public static void main(String[] args) {
            //静态代理测试
            RailwayStation railwayStation = new RailwayStation();
            railwayStation.getTicket();
    
            RailwayAgencyProxy railwayAgencyProxy = new RailwayAgencyProxy(railwayStation);
            railwayAgencyProxy.getTicket();
    
            //动态代理测试
            Ticket ticket = new RailwayStation();
            JDKDynamicProxy jdkDynamicProxy = new JDKDynamicProxy(ticket);
            Ticket proxyBuyTicket = (Ticket) jdkDynamicProxy.createProxyObj();
            proxyBuyTicket.getTicket();
        }
    }
    

    观察上面动态代理以及静态代理测试,动态代理的优势就显而易见了。如果我们再演示另外猪八戒和高翠兰代理场景的时候,是不是就不用再去创建GaocuiLanProxy了,我们只需要通过JDKDynamicProxy的方式去创建代理类即可。

    注意Proxy.newProxyInstance()方法接受三个参数:

    1. ClassLoader loader:指定当前目标对象使用的类加载器,获取加载器的方法是固定的。
    2. Class<?>[] interfaces:指定目标对象实现的接口的类型,使用泛型方式确认类型
    3. InvocationHandler:指定动态处理器,执行目标对象的方法时,会触发事件处理器的方法

    通过上面的例子以及上述参数,我们不难发现JDK动态代理有这样一个特性:
    JDK动态代理是面向接口的代理模式,如果要用JDK代理的话,首先得有个接口,例如上面例子中的Ticket接口

    cglib动态代理

    咱们再来看一下cglib动态代理。先了解一下cglib是什么。关于cglib,其实其官方解释是比较少的,但是其本身是十分强大的,这也是很多人所诟病的。CGLIB(Code Generation Library)是一个开源项目!是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。CGLIB是一个强大的高性能的代码生成包。它广泛的被许多AOP的框架使用,例如Spring AOP为他们提供方法的interception(拦截)。CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。除了CGLIB包,脚本语言例如Groovy和BeanShell,也是使用ASM来生成java的字节码。当然不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

    接下来我们看一下用法,由于cglib并不是jdk自带的,所以如果是maven项目的话,咱们首先需要的是引入cglib相关的pom依赖,如下:

    <dependency>    
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.3.0</version>
    </dependency>
    

    由于cglib代理的对象是类,这个是和JDK动态代理不一样的地方,这个地方标红加重点。
    这样的话,咱们同样的代理类的话则应该如下定义,

    public class Ticket {
        public void getTicket(){   
            System.out.println("买了张火车票");  
        }   
        
         final public void refundTicket(){  
            System.out.println("退了张火车票"); 
          }
     }
    

    显然,上面的类定义了两个方法,一个是买火车票另外的话退火车票。

    public class CglibDynamicProxy implements MethodInterceptor {    
            
        /**
         * @param o cglib生成的代理对象
         * @param method 被代理对象的方法
         * @param objects 传入方法的参数
         * @param methodProxy 代理的方法
         * @return
         * @throws Throwable
         */   
            public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                System.out.println("execute pre");
                Object obj = methodProxy.invokeSuper(o,objects);
                System.out.println("execute after");
                 return obj;
        }
    }
    

    调用测试入口方法调用

    public class TestCglibProxy {
        public static void main(String[] args) {
            // 代理类class文件存入本地磁盘方便我们反编译查看源码
       System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/kdaddy/project/log");
            // 通过CGLIB动态代理获取代理对象的过程
            Enhancer enhancer = new Enhancer();
            // 设置enhancer对象的父类
            enhancer.setSuperclass(Ticket.class);
            // 设置enhancer的回调对象
            enhancer.setCallback(new CglibDynamicProxy());
            // 创建代理对象
            Ticket ticket = (Ticket) enhancer.create();
            // 通过代理对象调用目标方法
            ticket.getTicket();
            // 通过代理尝试调用final对象调用目标方法
            ticket.refundTicket();
        }
    }
    

    运行之后我们得到的结果如下:

    execute pre
    买了张火车票
    execute after
    取消了张火车票
    

    根据日志的打印情况,我们很容易发现相关打印“取消了张火车票”并没有被代理,所以我们由此可以得到一个结论cglib动态代理无法代理被final修饰的方法

    上述源码中,我们提及将代理类class写到了相关的磁盘中,打开对应的目录,我们会发现下面三个文件

    源码文件Ticket$$EnhancerByCGLIB$$4e79a04a类为cglib生成的代理类,该类即成了Ticket。
    咱们来慢慢看看相关的源码:

    public class Ticket$$EnhancerByCGLIB$$4e79a04a extends Ticket implements Factory {
        private boolean CGLIB$BOUND;
        public static Object CGLIB$FACTORY_DATA;
        private static final ThreadLocal CGLIB$THREAD_CALLBACKS;
        private static final Callback[] CGLIB$STATIC_CALLBACKS;
        //拦截器
        private MethodInterceptor CGLIB$CALLBACK_0; 
        private static Object CGLIB$CALLBACK_FILTER;
        //被代理方法
        private static final Method CGLIB$getTicket$0$Method; 
        //代理方法
        private static final MethodProxy CGLIB$getTicket$0$Proxy;
        private static final Object[] CGLIB$emptyArgs;
        private static final Method CGLIB$equals$1$Method;
        private static final MethodProxy CGLIB$equals$1$Proxy;
        private static final Method CGLIB$toString$2$Method;
        private static final MethodProxy CGLIB$toString$2$Proxy;
        private static final Method CGLIB$hashCode$3$Method;
        private static final MethodProxy CGLIB$hashCode$3$Proxy;
        private static final Method CGLIB$clone$4$Method;
        private static final MethodProxy CGLIB$clone$4$Proxy;
    
        static void CGLIB$STATICHOOK1() {
            CGLIB$THREAD_CALLBACKS = new ThreadLocal();
            CGLIB$emptyArgs = new Object[0];
            Class var0 = Class.forName("kdaddy.com.cglibDynamic.Ticket$$EnhancerByCGLIB$$4e79a04a");
            Class var1;
            CGLIB$getTicket$0$Method = ReflectUtils.findMethods(new String[]{"getTicket", "()V"}, (var1 = Class.forName("kdaddy.com.cglibDynamic.Ticket")).getDeclaredMethods())[0];
            CGLIB$getTicket$0$Proxy = MethodProxy.create(var1, var0, "()V", "getTicket", "CGLIB$getTicket$0");
            Method[] var10000 = ReflectUtils.findMethods(new String[]{"equals", "(Ljava/lang/Object;)Z", "toString", "()Ljava/lang/String;", "hashCode", "()I", "clone", "()Ljava/lang/Object;"}, (var1 = Class.forName("java.lang.Object")).getDeclaredMethods());
            CGLIB$equals$1$Method = var10000[0];
            CGLIB$equals$1$Proxy = MethodProxy.create(var1, var0, "(Ljava/lang/Object;)Z", "equals", "CGLIB$equals$1");
            CGLIB$toString$2$Method = var10000[1];
            CGLIB$toString$2$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/String;", "toString", "CGLIB$toString$2");
            CGLIB$hashCode$3$Method = var10000[2];
            CGLIB$hashCode$3$Proxy = MethodProxy.create(var1, var0, "()I", "hashCode", "CGLIB$hashCode$3");
            CGLIB$clone$4$Method = var10000[3];
            CGLIB$clone$4$Proxy = MethodProxy.create(var1, var0, "()Ljava/lang/Object;", "clone", "CGLIB$clone$4");
        }
    }    
    

    我们通过代理类的源码可以看到,代理类会获得所有在父类继承来的方法,并且会有MethodProxy与之对应,当然被final修饰的方法除外,上述源码中我们也确实没有看到之前的refundTicket方法。接下来往下看。

    咱们看其中一个方法的调用。

    //代理方法(methodProxy.invokeSuper会调用)
    final void CGLIB$getTicket$0() {
            super.getTicket();
        }
    
    //被代理方法(methodProxy.invoke会调用,这就是为什么在拦截器中调用methodProxy.invoke会死循环,一直在调用拦截器)
        public final void getTicket() {
            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$getTicket$0$Method, CGLIB$emptyArgs, CGLIB$getTicket$0$Proxy);
            } else {
                super.getTicket();
            }
        }
    

    通过上述,我们看下getTicket整个的调用链路:
    调用getTicket()方法->调用拦截器->methodProxy.invokeSuper->CGLIB$getTicket$0->被代理的getTicket方法。

    接下来,咱们再来看一下比较核心的MethodProxy,咱们直接看下核心:methodProxy.invokeSuper,具体源码如下:

     public Object invokeSuper(Object obj, Object[] args) throws Throwable {
            try {
                this.init();
                MethodProxy.FastClassInfo fci = this.fastClassInfo;
                return fci.f2.invoke(fci.i2, obj, args);
            } catch (InvocationTargetException var4) {
                throw var4.getTargetException();
            }
        }
        
    //进一步看一下FastClassInfo
    private static class FastClassInfo {
            FastClass f1;//被代理类FastClass
            FastClass f2;//代理类FastClass
            int i1; //被代理类的方法签名(index)
            int i2;//代理类的方法签名
    
            private FastClassInfo() {
            }
        }
    

    上面代码调用过程就是获取到代理类对应的FastClass,并执行了代理方法。还记得之前生成三个class文件吗?Ticket$$EnhancerByCGLIB$$4e79a04a$$FastClassByCGLIB$$f000183.class就是代理类的FastClass,Ticket$$FastClassByCGLIB$$a79cabb2.class就是被代理类的FastClass。

    关于FastClass
    Cglib动态代理执行代理方法效率之所以比JDK的高是因为Cglib采用了FastClass机制,它的原理简单来说就是:为代理类和被代理类各生成一个Class,这个Class会为代理类或被代理类的方法分配一个index(int类型)。
    这个index当做一个入参,FastClass就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。下面我们反编译一个FastClass看看:

    public int getIndex(Signature var1) {
            String var10000 = var1.toString();
            switch(var10000.hashCode()) {
            case -80792013:
                if (var10000.equals("getTicket()V")) {
                    return 0;
                }
                break;
            case 189620111:
                if (var10000.equals("cancelTicket()V")) {
                    return 1;
                }
                break;
            case 1826985398:
                if (var10000.equals("equals(Ljava/lang/Object;)Z")) {
                    return 2;
                }
                break;
            case 1913648695:
                if (var10000.equals("toString()Ljava/lang/String;")) {
                    return 3;
                }
                break;
            case 1984935277:
                if (var10000.equals("hashCode()I")) {
                    return 4;
                }
            }
    
            return -1;
        }
        ...此处省略部分代码...
        public Object invoke(int var1, Object var2, Object[] var3) throws InvocationTargetException {
            Ticket var10000 = (Ticket)var2;
            int var10001 = var1;
    
            try {
                switch(var10001) {
                case 0:
                    var10000.getTicket();
                    return null;
                case 1:
                    var10000.cancelTicket();
                    return null;
                case 2:
                    return new Boolean(var10000.equals(var3[0]));
                case 3:
                    return var10000.toString();
                case 4:
                    return new Integer(var10000.hashCode());
                }
            } catch (Throwable var4) {
                throw new InvocationTargetException(var4);
            }
    
            throw new IllegalArgumentException("Cannot find matching method/constructor");
        }
        
    

    FastClass并不是跟代理类一块生成的,而是在第一次执行MethodProxy invoke/invokeSuper时生成的并放在了缓存中。

    //MethodProxy invoke/invokeSuper都调用了init()
    private void init() {
            if(this.fastClassInfo == null) {
                Object var1 = this.initLock;
                synchronized(this.initLock) {
                    if(this.fastClassInfo == null) {
                        MethodProxy.CreateInfo ci = this.createInfo;
                        MethodProxy.FastClassInfo fci = new MethodProxy.FastClassInfo();
                        fci.f1 = helper(ci, ci.c1);//如果缓存中就取出,没有就生成新的FastClass,此处感兴趣的小伙伴可以仔细看一下底层源码,老猫此处提及一下。
                        fci.f2 = helper(ci, ci.c2);
                        fci.i1 = fci.f1.getIndex(this.sig1);//获取方法的index
                        fci.i2 = fci.f2.getIndex(this.sig2);
                        this.fastClassInfo = fci;
                        this.createInfo = null;
                    }
                }
            }
    
        }
    

    总结

    如果屏幕前的你也像张小帅那样,该如何应对呢?其实大部分的答案都在上面了。总结一下
    (1)JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象。
    (2)JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。
    (3)JDK调用代理方法,是通过反射机制调用,Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。

    我是老猫,更多内容,欢迎大家搜索关注老猫的公众号“程序员老猫”。

    热爱技术,热爱产品,热爱生活,一个懂技术,懂产品,懂生活的程序员~ 更多精彩内容,可以关注公众号“程序员老猫”。 一起讨论技术,探讨一下点子,研究研究赚钱!
  • 相关阅读:
    Robin Hood CodeForces
    Arthur and Questions CodeForces
    AC日记——过河卒 洛谷 1002
    加密(模拟)
    AC日记——codevs 1086 栈 (卡特兰数)
    AC日记——搞笑世界杯 codevs 1060(dp)
    AC日记—— codevs 1031 质数环(搜索)
    AC日记——产生数 codevs 1009 (弗洛伊德)(组合数学)
    AC日记——阶乘之和 洛谷 P1009(高精度)
    AC日记——逃跑的拉尔夫 codevs 1026 (搜索)
  • 原文地址:https://www.cnblogs.com/kdaddy/p/15743123.html
Copyright © 2020-2023  润新知