动态代理大揭秘,带你彻底弄清楚动态代理!

前言

代理模式是一种设计模式,能够使得在不修改源目标的前提下,额外扩展源目标的功能。即通过访问源目标的代理类,再由代理类去访问源目标。这样一来,要扩展功能,就无需修改源目标的代码了。只需要在代理类上增加就可以了。

动态代理大揭秘,带你彻底弄清楚动态代理!

其实代理模式的核心思想就是这么简单,在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点:

动态代理大揭秘,带你彻底弄清楚动态代理!
* 为什么这里 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/proxy-demo

如果你已经看到这,觉得此篇文章能帮助到你的话,请给文章点赞且分享,也希望能关注我的公众号。我是一个开源作者,会在空余时间分享技术和生活,和你一起成长。

动态代理大揭秘,带你彻底弄清楚动态代理!

Original: https://www.cnblogs.com/bryan31/p/15266725.html
Author: 铂赛东
Title: 动态代理大揭秘,带你彻底弄清楚动态代理!

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/586245/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • cenos7搭建gitlab

    git、github和gitlab的区别 git:是一种版本控制系统,是一个命令,是一种工具 gitlib:是基于实现功能的开发库 github:是一个基于git实现的在线代码仓库…

    Java 2023年6月7日
    083
  • idea启动 org.springframework.boot.web.server.PortInUseException: Port XXX is already in use

    win+r,输入cmd,进入命令行窗口查询占用端口号所在进程:netstat -ano|findstr 8001杀死进程:taskkill -f -pid 进程号 作者:crazy…

    Java 2023年6月13日
    076
  • 设计模式笔记(二):策略模式

    应用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一…

    Java 2023年6月6日
    088
  • ch04 Java流程控制

    Java 流程控制 Scanner对象 通过Scanner类的next()与nextLine()方法获取输入的字符串,在读取前一般使用hasNext()与hasNextLine()…

    Java 2023年6月9日
    067
  • Java Native Interface Specification—Contents

    http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html Original: https…

    Java 2023年5月29日
    0102
  • C语言快速上手

    C语言快速上手 本文旨在快速回顾C语言语法知识 char,short,int,long,long long #include int main() { printf("s…

    Java 2023年6月7日
    071
  • springmvc学习笔记2

    一、Controller和RestFul 第一步:配置web.xml 第二步:spring_mvc_servlet.xml 参考文档:https://blog.csdn.net/E…

    Java 2023年6月7日
    068
  • 通俗易懂讲反射

    可进入本人语雀文档看,格式更清晰明了哦 https://www.yuque.com/docs/share/3c013ec6-6c35-4854-aaf6-ff9a6e8a6af2?…

    Java 2023年6月8日
    0103
  • 基于java实现的简单区块链

    使用java创建第一个非常基本的区块链 实现一个简单的工作量证明系统即挖矿 区块链就是一串或者是一系列区块的集合,类似于链表的概念,每个区块都指向于后面一个区块,然后顺序的连接在一…

    Java 2023年5月29日
    076
  • [Java] HashMap 源码简要分析

    允许null作为key/value。 不保证按照插入的顺序输出。使用hash构造的映射一般来讲是无序的。 非线程安全。 内部原理与Hashtable类似。 源码简要分析 java;…

    Java 2023年5月29日
    072
  • Linux:修改Swap分区大小

    1、前言 先查看Swap分区大小 free -h如果创建过Swap,那么首先关闭Swap功能 swapoff -a,后续修改完再 swapon -a 2、创建Swap分区 创建分区…

    Java 2023年6月7日
    076
  • MyBatis 和 jeesite多表查询

    有时候经常碰到多级联查,比如通过某个功能A表查角色信息,但是A表和角色表没有直接的关联关系,需要通过用户表进行关联,所以就需要多级关联查询出来了(下面的只是举例,实际应用用户和角色…

    Java 2023年6月5日
    085
  • Java中@Qualifier注解

    当使用@Autowired注解按照组件类型进行注入时,若存在多个相同类型的组件时,spring就不知道该注入哪个了。此时就可以在多个相同类型的组件上使用@Component(&#8…

    Java 2023年5月29日
    094
  • 使用 sed 处理文本文件

    sed 是一款 GNU 流编辑器,可以按照指定的规则去处理文本文件或流,其强大的功能使用户在命令中快捷地修改文本文件成为可能。 它不会修改文件,除非使用shell重定向来保存结果。…

    Java 2023年6月7日
    0112
  • ELK安装过程中一些注意的地方

    安装流程比较简单,只需要下载安装包,解压安装包,修改配置文件,然后启动组件即可,但还是遇到一些小问题,这里做一下记录。 各个组件版本号需要保持一样,例如都使用 7.1.1版本 es…

    Java 2023年6月5日
    053
  • java多线程回顾1:线程的概念与创建

    现在几乎所有操作系统都支持多任务,通常一个任务就是一个程序,一个运行中的程序就是一个进程。当一个程序行时,其内部也可能在执行多个任务,进程内每一个任务的执行流,就是一个线程。 所以…

    Java 2023年6月15日
    084
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球