zl程序教程

您现在的位置是:首页 >  后端

当前栏目

谁与争锋,JDK动态代理大战CGLib动态代理

JDK代理 动态 大战 cglib
2023-09-14 09:15:23 时间

一、前言

Java三种代理模式和两种动态代理模式
Java的三种代理模式:静态代理 动态代理(动态代理也叫做:JDK代理,接口代理) Cglib代理
Java两种动态代理模式:JDK代理、CGLIB代理
Spring的AOP可以使用JDK代理,也可以使用CGLIB代理,前者基于接口,后者是基于子类。
JDK和Cglib实现动态代理优缺点分析
使用JDK动态代理,目标类必须实现的某个接口,如果某个类没有实现接口则不能生成代理对象。
使用Cglib动态原理,必须针对目标类生成一个子类,覆盖其中的所有方法,所以目标类和方法不能声明为final类型。
从执行效率上看,Cglib动态代理效率较高。

二、基本概念

首先,我们知道Spring AOP的底层实现有两种方式:一种是JDK动态代理,另一种是CGLib的方式。

自Java 1.3以后,Java提供了动态代理技术,允许开发者在运行期创建接口的代理实例,后来这项技术被用到了Spring的很多地方。

JDK动态代理主要涉及java.lang.reflect包下边的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑贬值在一起。

JDK动态代理的话,他有一个限制,就是它只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,如何创建动态代理实例哪?答案就是CGLib。

CGLib采用底层的字节码技术,全称是:Code Generation Library,CGLib可以为一个类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。

三、JDK 和 CGLib动态代理区别

3.1 JDK动态代理具体实现原理

通过实现InvocationHandlet接口创建自己的调用处理器;

通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理;

通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型;

通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入;

JDK动态代理是面向接口的代理模式,如果被代理目标没有接口那么Spring也无能为力,Spring通过Java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。

3.2 CGLib动态代理

CGLib是一个强大、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过 CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程呢。

3.3 两者对比

JDK动态代理是面向接口的。

CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败)。

3.4 使用注意

如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);

如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。

四、JDK 和 CGLib动态代理性能对比-教科书上的描述

我们不管是看书还是看文章亦或是我那个上搜索参考答案,可能很多时候,都可以找到如下的回答:

关于两者之间的性能的话,JDK动态代理所创建的代理对象,在以前的JDK版本中,性能并不是很高,虽然在高版本中JDK动态代理对象的性能得到了很大的提升,但是他也并不是适用于所有的场景。主要体现在如下的两个指标中:

1、CGLib所创建的动态代理对象在实际运行时候的性能要比JDK动态代理高不少,有研究表明,大概要高10倍;

2、但是CGLib在创建对象的时候所花费的时间却比JDK动态代理要多很多,有研究表明,大概有8倍的差距;

3、因此,对于singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反正,则比较适用JDK动态代理。

结果是不是如上边1、2、3条描述的那样哪?下边我们做一些小实验分析一下!

五、使用层面:性能测试 + 模拟JDK动态代理 + 模拟Cglib动态代理

1、首先有几个Java类

在这里插入图片描述

Target.java 是代理类和实际类的共同接口
TargetImpl.java 实际类,实现了接口
JdkDynamicProxyTest.java JDK动态代理
CglibProxyTest.java Cglib动态代理
ProxyPerformanceTest.java main()方法测试运行

2、Target.java 是代理类和实际类的共同接口

package com.java.proxy.test;

public interface Target {

    int test(int i);

}

3、TargetImpl.java 实际类,实现了接口

package com.java.proxy.test;

public class TargetImpl implements Target {

    @Override
    public int test(int i) {
        return i + 1;
    }
}

4、JdkDynamicProxyTest.java

package com.java.proxy.test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkDynamicProxyTest implements InvocationHandler {

    private Target target;   // 测试中注入接口引用target

    private JdkDynamicProxyTest(Target target) {
        this.target = target;
    }

    // 新建Jdk动态代理对象
    public static Target newProxyInstance(Target target) {   // target实参
        return (Target) Proxy.newProxyInstance(JdkDynamicProxyTest.class.getClassLoader(),
                new Class<?>[]{Target.class},   // 这里一定是接口
                new JdkDynamicProxyTest(target));   // target实参用来初始化JDK动态代理构造函数
    }

    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);   // 使用反射的方式调用方法  
        // 又方法签名=方法名+参数列表
        // 三个参数  proxy 没用到  method 方法名 args 参数列表 
        // target newProxyInstance()方法中,构造函数新建JDK动态代理对象的时候由调用方main()方法确定
    }
}

JDK动态代理原理:
实现InvocationHandler接口,重写invoke()方法
使用Proxy.newProxyInstance() 新建动态代理对象
使用反射方式invoke() 调用方法

5、CglibProxyTest.java Cglib动态代理

package com.java.proxy.test;

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibProxyTest implements MethodInterceptor {

    private CglibProxyTest() {   // 这里就没有注入Target引用了
    }
    // 新建Cglib动态代理对象
    public static <T extends Target> Target newProxyInstance(Class<T> targetInstanceClazz) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetInstanceClazz);  // 实参为父类/父接口字节码
        enhancer.setCallback(new CglibProxyTest());   // 构建Cglib动态代理对象
        return (Target) enhancer.create();  // 返回
    }
    // 调用方法
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        return proxy.invokeSuper(obj, args);   // 传递过来的代理调用方法
        // 方法签名=方法名+形参列表
        // 参数 obj
        // 参数 method  方法名
        // 参数 args  参数列表
        // 参数 proxy
    }

}

Cglib动态代理原理:
实现MethodInterceptor 接口,重写intercept()方法
使用enhancer.create(); 新建动态代理对象
intercept()方法中,使用invokeSuper() 调用方法

注意:InvocationHandler 接口 和 MethodInterceptor 接口 都是 Callback 子接口,Callback接口没有方法,InvocationHandler接口是invoke()方法,MethodInterceptor 接口是intercept()方法。
在这里插入图片描述

6、ProxyPerformanceTest.java main()方法测试运行

package com.java.proxy.test;

import java.util.LinkedHashMap;
import java.util.Map;

public class ProxyPerformanceTest {

    public static void main(String[] args) {
        //创建测试对象
        Target nativeTest = new TargetImpl();   // 实际类
        Target dynamicProxy = JdkDynamicProxyTest.newProxyInstance(nativeTest);  // Jdk动态代理
        Target cglibProxy = CglibProxyTest.newProxyInstance(TargetImpl.class);  // Cglib动态代理

        //预热一下  没什么用
        int preRunCount = 10000;  
        runWithoutMonitor(nativeTest, preRunCount);  // 两个参数的,没啥用
        runWithoutMonitor(cglibProxy, preRunCount);
        runWithoutMonitor(dynamicProxy, preRunCount);

        //执行测试
        Map<String, Target> tests = new LinkedHashMap<String, Target>();
        tests.put("Native   ", nativeTest);    // map类型的tests,里面放三个,第二层循环为3个   不使用代理
        tests.put("Dynamic  ", dynamicProxy);   //jdk代理
        tests.put("Cglib    ", cglibProxy);    // cglib代理
        int repeatCount = 3;  // 重复次数为3  第一层循环
        int runCount = 1000000;   // 第二层循环里面执行100万次
        runTest(repeatCount, runCount, tests);  
        runCount = 50000000;   // 第二层循环里面执行5000万次
        runTest(repeatCount, runCount, tests);
    }
    
    // runTest()方法,被main()方法调用
    private static void runTest(int repeatCount, int runCount, Map<String, Target> tests) {    
       // 起手式:打印
        System.out.println(
                String.format("\n===== run test : [repeatCount=%s] [runCount=%s] [java.version=%s] =====",
                        repeatCount, runCount, System.getProperty("java.version")));
            // 循环重复次数 repeatCount            
        for (int i = 0; i < repeatCount; i++) {
            System.out.println(String.format("\n--------- test : [%s] ---------", (i + 1)));    // 每一次循环都打印
            for (String key : tests.keySet()) {  // 对于map类型的tests调用
                runWithMonitor(tests.get(key), runCount, key);    // 调用三个参数的这个方法,可以打印执行时间
            }
        }
    }

    private static void runWithoutMonitor(Target target, int runCount) {  // 没有tag参数
        for (int i = 0; i < runCount; i++) {
            target.test(i);    // 执行runCount次,test()方法
        }
    }

    private static void runWithMonitor(Target target, int runCount, String tag) {  // 有tag参数
        long start = System.currentTimeMillis();  // 开始时间
        for (int i = 0; i < runCount; i++) {
            target.test(i);   // 执行runCount次,test()方法
        }
        long end = System.currentTimeMillis();   // 结束时间
        System.out.println("[" + tag + "] Total Time:" + (end - start) + "ms");  // 打印tag 和 所用时间(end-start)
    }
}

7、测试结果

(1)JDK 1.6

在这里插入图片描述
在这里插入图片描述

JDK6这种情况下,cglib代理比jdk代理快,当然,不使用代理才是最快的,毕竟少了一层。

(2)JDK 1.7

在这里插入图片描述
在这里插入图片描述

JDK7这种情况下,cglib代理比jdk代理快,当然,不使用代理才是最快的,毕竟少了一层。

(3)JDK 1.8

在这里插入图片描述
在这里插入图片描述

JDK8这种情况下,jdk代理比cglib代理快,当然,不使用代理才是最快的,毕竟少了一层。

干货(面试回答)

(1)在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距;

(2)在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了;

六、原理层面:Fastclass机制

Jdk动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率比较低,所以cglib采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

问题:为什么cglib代理比jdk代理快?
Jdk动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率比较低,所以cglib采用了FastClass的机制来实现对被拦截方法的调用。FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

6.1 cglib代理使用Fastclass机制

下面用一个小例子来说明一下,这样比较直观:

public class test10 {
    public static void main(String[] args){
        Test tt = new Test();    // main()方法 新建Test对象
        Test2 fc = new Test2();    //  新建Test2对象
        int index = fc.getIndex("f()V");  // Test2对象,调用getIndex()方法,根据哈希值,得到index
        fc.invoke(index, tt, null);  // Test2对象,使用得到的index,调用方法
    }
}


class Test{  // Test类两个方法,f()方法和g()方法
    public void f(){
        System.out.println("f method");
    }
    
    public void g(){
        System.out.println("g method");
    }
}
class Test2{   
    public Object invoke(int index, Object o, Object[] ol){
        Test t = (Test) o;  
        switch(index){
        case 1:
            t.f();  // invoke()方法中,调用f()方法,不是反射调用,直接调用,反射调用就速度慢了
            return null;
        case 2:
            t.g();   // invoke()方法中,调用g()方法,不是反射调用,直接调用,反射调用就速度慢了
            return null;
        }
        return null;
    }
    
    public int getIndex(String signature){  // 根据哈希值得到index
        switch(signature.hashCode()){
        case 3078479:   
            return 1;   
        case 3108270:
            return 2;
        }
        return -1;
    }
}

上例中,Test2是Test的Fastclass

FastClass有两个方法getIndex()和invoke()。
getIndex()方法:根据入参(方法名+方法的描述符),对Test的每个方法建立索引,并
返回。
invoke()方法:根据指定的索引,以ol为入参调用对象O的方法。这样就避免了反射调用,提高了效率。

代理类(
在这里插入图片描述

)中与生成Fastclass相关的代码如下:

Class localClass1 = Class.forName("net.sf.cglib.test.Target$$EnhancerByCGLIB$$788444a0");
localClass2 = Class.forName("net.sf.cglib.test.Target");
CGLIB$g$0$Proxy = MethodProxy.create(localClass2, localClass1, "()V", "g", "CGLIB$g$0");

MethodProxy中会对localClass1和localClass2进行分析并生成FastClass,然后再使用getIndex来获取方法g 和 CGLIB$g$0的索引,具体的生成过程将在后续进行介绍,这里介绍一个关键的内部类:

 private static class FastClassInfo
    {
        FastClass f1; // net.sf.cglib.test.Target的fastclass
        FastClass f2; // Target$$EnhancerByCGLIB$$788444a0 的fastclass
        int i1; //方法g在f1中的索引
        int i2; //方法CGLIB$g$0在f2中的索引
    }

MethodProxy 中invokeSuper方法的代码如下:

FastClassInfo fci = fastClassInfo;
return fci.f2.invoke(fci.i2, obj, args);

当调用invokeSuper方法时,实际上是调用代理类的CGLIB$g 0 方 法 , C G L I B 0方法,CGLIB 0CGLIBg$0直接调用了目标类的g方法。所以,在第一节示例代码中我们使用invokeSuper方法来调用被拦截的目标类方法。

七、面试金手指

7.0 三种代理方式和两种动态代理方式

Java三种代理模式和两种动态代理模式
Java的三种代理模式:静态代理 动态代理(动态代理也叫做:JDK代理,接口代理) Cglib代理
Java两种动态代理模式:JDK代理、CGLIB代理
Spring的AOP可以使用JDK代理,也可以使用CGLIB代理,前者基于接口,后者是基于子类。
JDK和Cglib实现动态代理优缺点分析
使用JDK动态代理,目标类必须实现的某个接口,如果某个类没有实现接口则不能生成代理对象。
使用Cglib动态原理,必须针对目标类生成一个子类,覆盖其中的所有方法,所以目标类和方法不能声明为final类型。
从执行效率上看,Cglib动态代理效率较高。

7.1 jdk动态代理和cglib动态代理

7.1.1 jdk动态代理和cglib动态代理实现

JDK动态代理原理:
实现InvocationHandler接口,重写invoke()方法
使用Proxy.newProxyInstance() 新建动态代理对象
使用反射方式invoke() 调用方法

Cglib动态代理原理:
实现MethodInterceptor 接口,重写intercept()方法
使用enhancer.create(); 新建动态代理对象:步骤:
(1)创建Enhancer实例
(2)通过setSuperclass方法来设置目标类
(3)通过setCallback 方法来设置拦截对象
(4)create方法生成Target的代理类,并返回代理类的实例
intercept()方法中,使用invokeSuper() 调用方法

注意:InvocationHandler 接口 和 MethodInterceptor 接口 都是 Callback 子接口,Callback接口没有方法,InvocationHandler接口是invoke()方法,MethodInterceptor 接口是intercept()方法。

7.1.2 jdk动态代理和cglib动态代理的优缺点

jdk动态代理
具体来讲就三个步骤:
1.根据ClassLoader和Interface来获取接口类(前面已经讲了,类是由ClassLoader加载到JVM的,所以通过ClassLoader和Interface可以找到接口类)
2.获取构造对象;
3.通过构造对象和InvocationHandler生成实例,并返回,就是我们要的代理类。
Java动态代理优缺点:
优点:
1.Java本身支持,不用担心依赖问题,随着版本稳定升级;
2.代码实现简单;
缺点:
1.目标类必须实现某个接口,换言之,没有实现接口的类是不能生成代理对象的;
2.代理的方法必须都声明在接口中,否则,无法代理;
3.执行速度性能相对cglib较低;

cglib动态代理
Cglib原理:
1.通过字节码增强技术动态的创建代理对象;
2.代理的是代理对象的引用;
Cglib优缺点:
优点:
1.代理的类无需实现接口;
2.执行速度相对JDK动态代理较高;
缺点:
1.字节码库需要进行更新以保证在新版java上能运行;
2.动态创建代理对象的代价相对JDK动态代理较高;
Tips:
1.代理的对象不能是final关键字修饰的

7.2 cglib性能为什么比jdk动态代理性能高

分为两种情况:

(1)在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距;

(2)在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了

7.3 Fastclass机制描述下,哪些方法不能被动态代理

7.3.1 什么是Fastclass机制?为什么cglib动态代理比jdk动态代理要快?

FastClass有两个方法getIndex()和invoke()。
getIndex()方法:根据入参(方法名+方法的描述符),对Test的每个方法建立索引,并返回。
invoke()方法:根据指定的索引,以ol为入参调用对象O的方法。这样就避免了反射调用,提高了效率。

问题:为什么cglib代理比jdk代理快?
Jdk动态代理的拦截对象是通过反射的机制来调用被拦截方法的,反射的效率比较低;
cglib采用了FastClass的机制来实现对被拦截方法的调用,FastClass机制就是对一个类的方法建立索引,通过索引来直接调用相应的方法。

7.3.2 Fastclass机制描述下,三个方法不能被动态代理

Object类中
equals()方法、hashcode()方法、getClass()方法。

八、小结

两种代理方式,JDK和CGLib动态代理(三个问题),完成了

天天打码,天天进步!!!