关于java:动态代理大揭秘带你彻底弄清楚动态代理

4次阅读

共计 10205 个字符,预计需要花费 26 分钟才能阅读完成。

前言

代理模式是一种设计模式,可能使得在不批改源指标的前提下,额定扩大源指标的性能。即通过拜访源指标的代理类,再由代理类去拜访源指标。这样一来,要扩大性能,就无需批改源指标的代码了。只须要在代理类上减少就能够了。

其实代理模式的核心思想就是这么简略,在 java 中,代理又分动态代理和动静代理 2 种,其中动静代理依据不同实现又辨别基于接口的的动静代理和基于子类的动静代理。

其中动态代理因为比较简单,面试中也没啥问的,在代理模式一块,问的最多就是动静代理,而且动静代理也是 spring aop 的核心思想,spring 其余很多性能也是通过动静代理来实现的,比方拦截器,事务管制等。

熟练掌握动静代理技术,能让你业务代码更加精简而优雅。如果你须要写一些中间件的话,那动静代理技术更是必不可少的技能包。

那此篇文章就带大家一窥动静代理的所有细节吧。

动态代理

在说动静代理前,还是先说说动态代理。

所谓动态代理,就是通过申明一个明确的代理类来拜访源对象。

咱们有 2 个接口,Person 和 Animal。每个接口各有 2 个实现类,UML 如下图:

每个实现类中代码都差不多统一,用 Student 来举例(其余类和这个简直截然不同)

public class Student implements Person{

    private String name;

    public Student() {}

    public Student(String name) {this.name = name;}

    @Override
    public void wakeup() {System.out.println(StrUtil.format("学生 [{}] 晚上醒来啦",name));
    }

    @Override
    public void sleep() {System.out.println(StrUtil.format("学生 [{}] 早晨睡觉啦",name));
    }
}

假如咱们当初要做一件事,就是在所有的实现类调用 wakeup() 前减少一行输入 早安~,调用 sleep() 前减少一行输入 晚安~。那咱们只须要编写 2 个代理类 PersonProxyAnimalProxy

PersonProxy:

public class PersonProxy implements Person {

    private Person person;

    public PersonProxy(Person person) {this.person = person;}

    @Override
    public void wakeup() {System.out.println("早安~");
        person.wakeup();}

    @Override
    public void sleep() {System.out.println("晚安~");
        person.sleep();}
}

AnimalProxy:

public class AnimalProxy implements Animal {

    private Animal animal;

    public AnimalProxy(Animal animal) {this.animal = animal;}

    @Override
    public void wakeup() {System.out.println("早安~");
        animal.wakeup();}

    @Override
    public void sleep() {System.out.println("晚安~");
        animal.sleep();}
}

最终执行代码为:

public static void main(String[] args) {Person student = new Student("张三");
    PersonProxy studentProxy = new PersonProxy(student);
    studentProxy.wakeup();
    studentProxy.sleep();

    Person doctor = new Doctor("王传授");
    PersonProxy doctorProxy = new PersonProxy(doctor);
    doctorProxy.wakeup();
    doctorProxy.sleep();

    Animal dog = new Dog("旺旺");
    AnimalProxy dogProxy = new AnimalProxy(dog);
    dogProxy.wakeup();
    dogProxy.sleep();

    Animal cat = new Cat("咪咪");
    AnimalProxy catProxy = new AnimalProxy(cat);
    catProxy.wakeup();
    catProxy.sleep();}

输入:

早安~
学生 [张三] 晚上醒来啦
晚安~
学生 [张三] 早晨睡觉啦
早安~
医生 [王传授] 晚上醒来啦
晚安~
医生 [王传授] 早晨睡觉啦
早安~~
小狗 [旺旺] 晚上醒来啦
晚安~~
小狗 [旺旺] 早晨睡觉啦
早安~~
小猫 [咪咪] 晚上醒来啦
晚安~~
小猫 [咪咪] 早晨睡觉啦

论断:

动态代理的代码置信曾经不必多说了,代码非常简单易懂。这里用了 2 个代理类,别离代理了 PersonAnimal接口。

这种模式尽管好了解,然而毛病也很显著:

  • 会存在大量的冗余的代理类,这里演示了 2 个接口,如果有 10 个接口,就必须定义 10 个代理类。
  • 不易保护,一旦接口更改,代理类和指标类都须要更改。

JDK 动静代理

动静代理,艰深点说就是:无需申明式的创立 java 代理类,而是在运行过程中生成 ” 虚构 ” 的代理类,被 ClassLoader 加载。从而防止了动态代理那样须要申明大量的代理类。

JDK 从 1.3 版本就开始反对动静代理类的创立。次要外围类只有 2 个:java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler

还是后面那个例子,用 JDK 动静代理类去实现的代码如下:

创立一个 JdkProxy 类,用于对立代理:

public class JdkProxy implements InvocationHandler {

    private Object bean;

    public JdkProxy(Object bean) {this.bean = bean;}

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();
        if (methodName.equals("wakeup")){System.out.println("早安~~~");
        }else if(methodName.equals("sleep")){System.out.println("晚安~~~");
        }

        return method.invoke(bean, args);
    }
}

执行代码:

public static void main(String[] args) {JdkProxy proxy = new JdkProxy(new Student("张三"));
    Person student = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Person.class}, proxy);
    student.wakeup();
    student.sleep();

    proxy = new JdkProxy(new Doctor("王传授"));
    Person doctor = (Person) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Person.class}, proxy);
    doctor.wakeup();
    doctor.sleep();

    proxy = new JdkProxy(new Dog("旺旺"));
    Animal dog = (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Animal.class}, proxy);
    dog.wakeup();
    dog.sleep();

    proxy = new JdkProxy(new Cat("咪咪"));
    Animal cat = (Animal) Proxy.newProxyInstance(proxy.getClass().getClassLoader(), new Class[]{Animal.class}, proxy);
    cat.wakeup();
    cat.sleep();}

解说:

能够看到,绝对于动态代理类来说,无论有多少接口,这里只须要一个代理类。外围代码也很简略。惟一须要留神的点有以下 2 点:

  • JDK 动静代理是须要申明接口的,创立一个动静代理类必须得给这个”虚构“的类一个接口。能够看到,这时候经动静代理类发明之后的每个 bean 曾经不是原来那个对象了。

  • 为什么这里 JdkProxy 还须要结构传入原有的 bean 呢?因为解决完附加的性能外,须要执行原有 bean 的办法,以实现 代理 的职责。

    这里 JdkProxy 最外围的办法就是

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable

    其中 proxy 为代理过之后的对象(并不是原对象),method 为被代理的办法,args 为办法的参数。

    如果你不传原有的 bean,间接用 method.invoke(proxy, args) 的话,那么就会陷入一个死循环。

能够代理什么

JDK 的动静代理是也平时大家应用的最多的一种代理形式。也叫做接口代理。前几天有一个小伙伴在群里问我,动静代理是否一次能够代理一个类,多个类可不可以。

JDK 动静代理说白了只是依据接口”凭空“来生成类,至于具体的执行,都被代理到了 InvocationHandler 的实现类里。上述例子我是须要继续执行原有 bean 的逻辑,才将原有的 bean 结构进来。只有你须要,你能够结构进任何对象到这个代理实现类。也就是说,你能够传入多个对象,或者说你什么类都不代理。只是为某一个接口”凭空“的生成多个代理实例,这多个代理实例最终都会进入InvocationHandler 的实现类来执行某一个段独特的代码。

所以,在以往的我的项目中的一个理论场景就是,我有多个以 yaml 定义的规定文件,通过对 yaml 文件的扫描,来为每个 yaml 规定文件生成一个动静代理类。而实现这个,我只须要当时定义一个接口,和定义 InvocationHandler 的实现类就能够了,同时把 yaml 解析过的对象传入。最终这些动静代理类都会进入 invoke 办法来执行某个独特的逻辑。

Cglib 动静代理

Spring 在 5.X 之前默认的动静代理实现始终是 jdk 动静代理。然而从 5.X 开始,spring 就开始默认应用 Cglib 来作为动静代理实现。并且 springboot 从 2.X 开始也转向了 Cglib 动静代理实现。

是什么导致了 spring 体系整体转投 Cglib 呢,jdk 动静代理又有什么毛病呢?

那么咱们当初就要来说下 Cglib 的动静代理。

Cglib 是一个开源我的项目,它的底层是字节码解决框架 ASM,Cglib 提供了比 jdk 更为弱小的动静代理。次要相比 jdk 动静代理的劣势有:

  • jdk 动静代理只能基于接口,代理生成的对象只能赋值给接口变量,而 Cglib 就不存在这个问题,Cglib 是通过生成子类来实现的,代理对象既能够赋值给实现类,又能够赋值给接口。
  • Cglib 速度比 jdk 动静代理更快,性能更好。

那何谓通过子类来实现呢?

还是后面那个例子,咱们要实现雷同的成果。代码如下

创立 CglibProxy 类,用于对立代理:

public class CglibProxy implements MethodInterceptor {private Enhancer enhancer = new Enhancer();

    private Object bean;

    public CglibProxy(Object bean) {this.bean = bean;}

    public Object getProxy(){
        // 设置须要创立子类的类
        enhancer.setSuperclass(bean.getClass());
        enhancer.setCallback(this);
        // 通过字节码技术动态创建子类实例
        return enhancer.create();}
    // 实现 MethodInterceptor 接口办法
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {String methodName = method.getName();
        if (methodName.equals("wakeup")){System.out.println("早安~~~");
        }else if(methodName.equals("sleep")){System.out.println("晚安~~~");
        }

        // 调用原 bean 的办法
        return method.invoke(bean,args);
    }
}

执行代码:

public static void main(String[] args) {CglibProxy proxy = new CglibProxy(new Student("张三"));
    Student student = (Student) proxy.getProxy();
    student.wakeup();
    student.sleep();

    proxy = new CglibProxy(new Doctor("王传授"));
    Doctor doctor = (Doctor) proxy.getProxy();
    doctor.wakeup();
    doctor.sleep();

    proxy = new CglibProxy(new Dog("旺旺"));
    Dog dog = (Dog) proxy.getProxy();
    dog.wakeup();
    dog.sleep();

    proxy = new CglibProxy(new Cat("咪咪"));
    Cat cat = (Cat) proxy.getProxy();
    cat.wakeup();
    cat.sleep();}

解说:

在这里用 Cglib 作为代理,其思路和 jdk 动静代理差不多。也须要把原始 bean 结构传入。起因下面有说,这里不多赘述。

要害的代码在这里

// 设置须要创立子类的类
enhancer.setSuperclass(bean.getClass());
enhancer.setCallback(this);
// 通过字节码技术动态创建子类实例
return enhancer.create();

能够看到,Cglib” 凭空 ” 的发明了一个原 bean 的子类,并把 Callback 指向了 this,也就是以后对象,也就是这个 proxy 对象。从而会调用 intercept 办法。而在 intercept 办法里,进行了附加性能的执行,最初还是调用了原始 bean 的相应办法。

在 debug 这个生成的代理对象时,咱们也能看到,Cglib 是凭空生成了原始 bean 的子类:

javassist 动静代理

Javassist 是一个开源的剖析、编辑和创立 Java 字节码的类库,能够间接编辑和生成 Java 生成的字节码。绝对于 bcel, asm 等这些工具,开发者不须要理解虚拟机指令,就能动静扭转类的构造,或者动静生成类。

在日常应用中,javassit 通常被用来动静批改字节码。它也能用来实现动静代理的性能。

话不多说,还是一样的例子,我用 javassist 动静代理来实现一遍

创立 JavassitProxy,用作对立代理:

public class JavassitProxy {

    private Object bean;

    public JavassitProxy(Object bean) {this.bean = bean;}

    public Object getProxy() throws IllegalAccessException, InstantiationException {ProxyFactory f = new ProxyFactory();
        f.setSuperclass(bean.getClass());
        f.setFilter(m -> ListUtil.toList("wakeup","sleep").contains(m.getName()));

        Class c = f.createClass();
        MethodHandler mi = (self, method, proceed, args) -> {String methodName = method.getName();
            if (methodName.equals("wakeup")){System.out.println("早安~~~");
            }else if(methodName.equals("sleep")){System.out.println("晚安~~~");
            }
            return method.invoke(bean, args);
        };
        Object proxy = c.newInstance();
        ((Proxy)proxy).setHandler(mi);
        return proxy;
    }
}

执行代码:

public static void main(String[] args) throws Exception{JavassitProxy proxy = new JavassitProxy(new Student("张三"));
    Student student = (Student) proxy.getProxy();
    student.wakeup();
    student.sleep();

    proxy = new JavassitProxy(new Doctor("王传授"));
    Doctor doctor = (Doctor) proxy.getProxy();
    doctor.wakeup();
    doctor.sleep();

    proxy = new JavassitProxy(new Dog("旺旺"));
    Dog dog = (Dog) proxy.getProxy();
    dog.wakeup();
    dog.sleep();

    proxy = new JavassitProxy(new Cat("咪咪"));
    Cat cat = (Cat) proxy.getProxy();
    cat.wakeup();
    cat.sleep();}

解说:

相熟的配方,相熟的滋味,大抵思路也是相似的。同样把原始 bean 结构传入。能够看到,javassist 也是用”凭空“生成子类的形式类来解决,代码的最初也是调用了原始 bean 的指标办法实现代理。

javaassit 比拟有特点的是,能够对所须要代理的办法用 filter 来设定,外面能够像 Criteria 结构器那样进行结构。其余的代码,如果你认真看了之前的代码演示,应该能很轻易看懂了。

ByteBuddy 动静代理

ByteBuddy,字节码伙计,一听就很牛逼有不。

ByteBuddy 也是一个赫赫有名的开源库,和 Cglib 一样,也是基于 ASM 实现。还有一个名气更大的库叫 Mockito,置信不少人用过这玩意写过测试用例,其外围就是基于 ByteBuddy 来实现的,能够动静生成 mock 类,十分不便。另外 ByteBuddy 另外一个大的利用就是 java agent,其次要作用就是在 class 被加载之前对其拦挡,插入本人的代码。

ByteBuddy 十分弱小,是一个神器。能够利用在很多场景。然而这里,只介绍用 ByteBuddy 来做动静代理,对于其余应用形式,可能要专门写一篇来讲述,这里先给本人挖个坑。

来,还是相熟的例子,相熟的配方。用 ByteBuddy 咱们再来实现一遍后面的例子

创立 ByteBuddyProxy,做对立代理:

public class ByteBuddyProxy {

    private Object bean;

    public ByteBuddyProxy(Object bean) {this.bean = bean;}

    public Object getProxy() throws Exception{Object object = new ByteBuddy().subclass(bean.getClass())
                .method(ElementMatchers.namedOneOf("wakeup","sleep"))
                .intercept(InvocationHandlerAdapter.of(new AopInvocationHandler(bean)))
                .make()
                .load(ByteBuddyProxy.class.getClassLoader())
                .getLoaded()
                .newInstance();
        return object;
    }

    public class AopInvocationHandler implements InvocationHandler {

        private Object bean;

        public AopInvocationHandler(Object bean) {this.bean = bean;}

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();
            if (methodName.equals("wakeup")){System.out.println("早安~~~");
            }else if(methodName.equals("sleep")){System.out.println("晚安~~~");
            }
            return method.invoke(bean, args);
        }
    }
}

执行代码:

public static void main(String[] args) throws Exception{ByteBuddyProxy proxy = new ByteBuddyProxy(new Student("张三"));
    Student student = (Student) proxy.getProxy();
    student.wakeup();
    student.sleep();

    proxy = new ByteBuddyProxy(new Doctor("王传授"));
    Doctor doctor = (Doctor) proxy.getProxy();
    doctor.wakeup();
    doctor.sleep();

    proxy = new ByteBuddyProxy(new Dog("旺旺"));
    Dog dog = (Dog) proxy.getProxy();
    dog.wakeup();
    dog.sleep();

    proxy = new ByteBuddyProxy(new Cat("咪咪"));
    Cat cat = (Cat) proxy.getProxy();
    cat.wakeup();
    cat.sleep();}

解说:

思路和之前还是一样,通过仔细观察代码,ByteBuddy 也是采纳了发明子类的形式来实现动静代理。

各种动静代理的性能如何

后面介绍了 4 种动静代理对于同一例子的实现。对于代理的模式能够分为 2 种:

  • JDK 动静代理采纳接口代理的模式,代理对象只能赋值给接口,容许多个接口
  • Cglib,Javassist,ByteBuddy 这些都是采纳了子类代理的模式,代理对象既能够赋值给接口,又能够复制给具体实现类

Spring5.X,Springboot2.X 只有都采纳了 Cglib 作为动静代理的实现,那是不是 cglib 性能是最好的呢?

我这里做了一个简略而粗犷的试验,间接把上述 4 段执行代码进行单线程同步循环多遍,用耗时来确定他们 4 个的性能。应该能看出些端倪。

JDK PROXY 循环 10000 遍所耗时:0.714970125 秒
Cglib 循环 10000 遍所耗时:0.434937833 秒
Javassist 循环 10000 遍所耗时:1.294194708 秒
ByteBuddy 循环 10000 遍所耗时:9.731999042 秒

执行的后果如上

从执行后果来看,确实是 cglib 成果最好。至于为什么 ByteBuddy 执行那么慢,不肯定是 ByteBuddy 性能差,也有可能是我测试代码写的有问题,没有找到正确的形式。所以这只能作为一个大抵的参考。

看来 Spring 抉择 Cglib 还是有情理的。

最初

动静代理技术对于一个常常写开源或是中间件的人来说,是一个神器。这种个性提供了一种新的解决形式。从而使得代码更加优雅而简略。动静代理对于了解 spring 的核心思想也有着莫大的帮忙,心愿对动静代理技术感兴趣的童鞋能试着去跑一遍示例中的代码,来增强了解。

最初附上本篇文章中所用到的全副代码,我曾经将其上传到 Gitee:

https://gitee.com/bryan31/pro…

如果你曾经看到这,感觉此篇文章能帮忙到你的话,请给文章点赞且分享,也心愿能关注我的公众号。我是一个开源作者,会在空余工夫分享技术和生存,和你一起成长。

正文完
 0