你必须要学会的动态代理


​动态代理有多重要?相信了解过Spring源码的同学一定会深有体会,这在 AOP 面向切面编程领域经常见。本文从故事开始讲起,由浅入深,从静态代理到动态代理再到动态代理的源码解析,相信大家会有所收获。

问题背景

话说小强作为一名资深的单身狗,终于也迎来了自己的春天,在他的穷追不舍下,同事小丽终于同意给他一次机会,给他一个月的考察期,如果在这一个月内表现良好,就正式和他交往,否则就不要再纠缠她。

别看小强母胎单身到现在,对女生的喜好可是没少研究,也知道追女生要送礼物,女生都喜欢化妆品,于是决定先送小丽一支口红,小丽一高兴或许就答应做他女盆友了呢,可是辗转反侧了一晚上,也没有挑到一支合适的,最后在同事小红那里得知,小丽一直喜欢韩国A公司的口红,可是这个牌子的口红在国内很难买到,这可急坏了小强。

为了心爱的美眉,赴汤蹈火也在所不辞,于是小强决定亲自前往韩国,去购买这支口红。可是看到来回的机票价格,不免又有些不舍,平时短裤都不舍得换,这次真的要大出血。但是一想到女神小丽收到口红后开心的扑到他怀里,又露出了姨妈般的笑容。最终小强决定前往韩国购买口红

口红工厂接口:

public interface KouHongFactory {
    public void saleKouHong(String color);
}

生产口红的A工厂:

public class AaFactory implements KouHongFactory{
    @Override
    public void saleKouHong(String color) {
        System.out.println("您购买的是"+color+"色号的口红");
    }
}

小强前往A工厂的柜台购买:

public class XiaoQiang {
    public static void main(String[] args) {
        //A公司生产一种口红,是女神喜欢的
        KouHongFactory factory = new AaFactory();
        //小强乘坐飞机,历经千辛万苦来到工厂
        //购买死亡芭比粉颜色的口红
        factory.saleKouHong("死亡芭比粉");
    }
}

有何问题

上面小强前往韩国购买口红,由于不懂韩语,在韩国兜了很大的圈子才找到这款口红的专卖店,真是吃尽了苦头。另外来回的机票又是大几千,还占用了美好的周末。而且当回来之后小强就直接把口红送给了小丽,由于没有精美的包装,少了一些惊喜和仪式感,并且最重要的是色号并不是小丽喜欢的,小丽并没有表现出多么的高兴,想象中的拥抱和香吻自然也一个都没有。总之这次送礼物没有达到理想的效果,费时费力又费钱。

解决方法

谁让他是打不死的小强呢,一次挫折并不能击垮他,他决定还要送礼物,挑选一支适合小丽的口红色号,但自己对色号又确实不懂。正当小强郁闷地刷着朋友圈时,突然看到小学同学小美发的朋友圈,原来小美去年嫁给了一个富二代,现在经常出国游玩,顺便帮朋友代购一些化妆品。这次小美正好在韩国,发朋友圈问有没有需要代购的朋友,还配了三张性感的写真照片,看的小强直流口水,想当初小美还是小强的梦中女郎呢,一想到自己的女神小丽身材也不错,又打起了斗志,赶紧联系小美,让她代购一支A公司的口红。和小美说了上次送口红的事,被小美好一顿嘲笑,小美决定帮他选择色号,让他发一张小丽的素颜照片,根据小丽肤色选择一款适合她的口红。

新加的小美做代理,同样继承口红工厂接口

//代理对象,和真实对象继承与同一个接口
public class XiaoMei implements KouHongFactory {

    //小美不生产口红,所有要包含真实的口红厂商
    public KouHongFactory factory;

    public XiaoMei(KouHongFactory factory){
        super();
        this.factory = factory;
    }

    @Override
    public void saleKouHong(String color) {
        System.out.println("您想要购买"+color+"色号的口红");
        //前置增强,推荐一种色号
        color = before();
        factory.saleKouHong(color);
        //后置增强
        after();
    }
    public String before(){
        String color = "姨妈红";
        System.out.println("为您推荐的色号为"+color);
        return color;
    }
    public void after(){
        System.out.println("为您精美包装,免费邮寄,七天可退换");
    }
}

小强类

package Proxy;

public class XiaoQiang {
    public static void main(String[] args) {
        //A公司生产一种口红,是女神喜欢的
        KouHongFactory factory = new AaFactory();
        //小美是代理
        XiaoMei xiaoMei  = new XiaoMei(factory);
        //小强让小美帮忙买一支死亡芭比粉颜色的口红
        xiaoMei.saleKouHong("死亡芭比粉");

    }
}

运行结果:

您想要购买死亡芭比粉色号的口红
为您推荐的色号为姨妈红
您购买的是姨妈红色号的口红
为您精美包装,免费邮寄,七天可退换

从结果中可以看出,小美并没有按照小强要求购买死亡芭比粉色号的口红,而是买了一支更适合小丽肤色的姨妈红色号的口红。并且提供了精美包装和免费邮寄,还可以七天可退换的售后服务。简直是一条龙服务,从买前咨询,到售后服务。小强收到快递后,拿着精美包装的口红,约小丽出来吃饭,并在吃饭的时候将口红送给了她,这次女神终于开心的笑了,看到女神的笑容,小强连孩子的名字都想好了。

模式讲解

代理模式定义:给目标对象提供一个代理对象,并由代理对象控制对目标对象的访问。

目的:

  • 通过引入代理对象的方式间接地访问目标对象,防止直接访问目标对象给系统带来的不必要的复杂性。
  • 通过代理对象对原有的业务进行增强。

小美作为代理对A公司的业务进行了增强,并没有修改A公司的代码,是一种无侵入式的增强。第二点并没有影响客户端的调用,也就是小强的购买过程所需要的参数不变。

新的问题

小丽对小强热情了没几天,又开始变的冷淡了,聊天总是回复哦和嗯,想约出来吃饭也是各种理由推脱,这下小强又陷入了苦恼之中,想着要不要再送一些礼物给她,既然口红已经送过了,就送一盒粉底和一套水乳吧,女神一定会喜欢。

于是他又联系了小美,这次小美正好在日本度假,过两天就回国,他把自己想买粉底和水乳的想法告诉了小美,可是小美目前却并没有代购这些产品,但是在小强的苦苦哀求下,说什么下辈子的幸福就全靠小美了,还望成全。本着宁拆十座庙,不毁一门婚的原则,小美就答应帮他代购粉底和水乳了。

粉底接口:

public interface FenDiFactory {

    public void saleFenDi(String color);
}

B公司生产粉底并销售:

public class BbFactory implements FenDiFactory{

    @Override
    public void saleFenDi(String color) {
        System.out.println("您购买的是"+color+"颜色的粉底");
    }
}

这个时候小美该如何处理呢?继续加实现的接口?像下面这样,那如果还要代理水乳呢?接着实现接口吗,显然不合理,这违反了开闭原则(对扩展开放,对修改关闭)

public class XiaoMei implements KouHongFactory,FenDiFactory{
}

这时候小美已经开始忙不过来了,一边需要赶往韩国帮别人代购着口红,又要去日本帮小强代购粉底,于是她打算招几名员工,帮她跑腿,有的去韩国代购口红,有的去日本代购粉底,别忘了她老公可是富二代啊,有的是钱。说干就干,于是小美成立了小美代购公司,目前可以代购口红,粉底,水乳等多种化妆品。

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

public class XiaoMeiCompany implements InvocationHandler {

    //被代理的对象,为了通用使用Object类型,可以代理各种公司
    private Object factory;

    public Object getFactory(){
        return factory;
    }

    public void setFactory(Object factory){
        this.factory = factory;
    }
    //通过Proxy获取动态代理的对象
    public Object getProxyInstance(){
        return Proxy.newProxyInstance(factory.getClass().getClassLoader(),factory.getClass().getInterfaces(),this);
    }
    //通过动态代理对象对方法进行增强
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        before();
        //为了提高通用性,使用反射,因为可能需要增强很多方法,method为被增强的方法
        Object ret = method.invoke(factory,objects);
        after();
        return ret;
    }

    public void before(){
        System.out.println("您好,感谢咨询,请问有什么可以帮助到您的");
    }
    public void after(){
        System.out.println("为您精美包装,免费邮寄,七天可退换");
    }
}

这里使用到了Proxy类和InvocationHandler接口,需要简单的介绍一下:

在java中,代理公司就相当于JVM,它可以动态生成代理对象,相当于指定一名员工为某个客户服务。

Proxy提供一个静态方法,负责生成动态代理类以及它的实例,静态方法为newProxyInstance,源码如下:

@CallerSensitive
    public static Object newProxyInstance(ClassLoader var0, Class<?>[] var1, InvocationHandler var2) throws IllegalArgumentException {
        Objects.requireNonNull(var2);
        Class[] var3 = (Class[])var1.clone();
        SecurityManager var4 = System.getSecurityManager();
        if (var4 != null) {
            checkProxyAccess(Reflection.getCallerClass(), var0, var3);
        }
        Class var5 = getProxyClass0(var0, var3);
        try {
            if (var4 != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), var5);
            }
            final Constructor var6 = var5.getConstructor(constructorParams);
            if (!Modifier.isPublic(var5.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        var6.setAccessible(true);
                        return null;
                    }
                });
            }
            return var6.newInstance(var2);
            ...
    }

该方法返回指定接口的代理实例,该代理实例将方法调用调度到指定的调用处理程序。

InvocationHandler是一个接口,每个代理实例都有一个关联的调用处理程序。在代理实例上调用方法时,该方法调用将被编码并分派到invoke 其调用处理程序的方法。invoke的功能是处理代理实例上的方法调用并返回结果。

public interface InvocationHandler {
    Object invoke(Object var1, Method var2, Object[] var3) throws Throwable;
}

简单来说InvocationHandler接口相当于一种公司里的规范,invoke方法里规定了公司做一些什么事情,如在这个例子中,invoke里除了有本身的业务之外(代购商品)还有前置增强(买前提供咨询)和后置增强(包邮,售后服务),这些都属于一个公司的规范,由小美制定的,所有员工都必须遵守。

newProxyInstance中的三个参数,factory.getClass().getClassLoader()表示类加载器,factory.getClass().getInterfaces()表示实现的指定接口,this代表InvocationHandler。

InvocationHandler只负责增强业务,Proxy只负责创建对象,放在这个例子中就是Proxy用来指定公司里的员工,InvocationHandler规定了该员工对客户做哪些服务(买前咨询,代购,包邮,售后等)。这里也体现了单一职责原则。

小强通过小美的公司购买口红和粉底:

public class XiaoQiang {
    public static void main(String[] args) {
        //A公司生产一种口红,是女神喜欢的
        KouHongFactory afactory = new AaFactory();
        //B公司生产粉底
        FenDiFactory bfactory = new BbFactory();
        //小美的代购公司
        XiaoMeiCompany company = new XiaoMeiCompany();
        //代理A公司的口红
        company.setFactory(afactory);
        //命令1号员工去帮小强买口红
        KouHongFactory staff1 = (KouHongFactory) company.getProxyInstance();
        staff1.saleKouHong("姨妈红");
        System.out.println("---------------");

        company.setFactory(bfactory);
        //命令2号员工去帮小强买粉底
        FenDiFactory staff2 = (FenDiFactory) company.getProxyInstance();
        staff2.saleFenDi("象牙白");
    }
}

运行结果如下:

您好,感谢咨询,请问有什么可以帮助到您的
您购买的是姨妈红色号的口红
为您精美包装,免费邮寄,七天可退换
---------------
您好,感谢咨询,请问有什么可以帮助到您的
您购买的是象牙白颜色的粉底
为您精美包装,免费邮寄,七天可退换

代理对象的类图如下:
在这里插入图片描述

动态代理的原理

首先看一下类的完整生命周期:

在这里插入图片描述

如图所示,动态代理生成动态对象是在哪个阶段完成的呢,是在编译阶段,因为动态代理的对象并没有对应的java源文件,那么字节码又是怎么生成的呢?生成字节码有下面三种方式:

1.从硬盘,编译java源文件。

2.从网络,如tomacat的热加载。

3.从内存,如动态代理。

JVM如何在内存中生成字节码的呢?来分析一下源码,首先入口就是我们调用的Proxy.newProxyInstance

在这里插入图片描述
点进去查看newProxyInstance方法,如下:
在这里插入图片描述

上面画红线部分为关键,第一处为生成Java字节码和Class对象,后面两处为生成实例对象。我们要了解的重点就是如何生成的java字节码和Class对象,点进去查看getProxyClass0方法:
在这里插入图片描述

重点还是划线部分,继续查看这行代码的功能,点进get方法进入源码:
在这里插入图片描述

核心方法apply():
在这里插入图片描述

接收的第一个参数为类加载器,第二个是一个需要实现的接口数组,然后遍历这些接口做一些检查,检查部分的代码省略,再下面
在这里插入图片描述

生成一个序号,并且以包名+$Proxy+序号的格式生成了代理类的名字。然后生成java字节码和Class对象。生成字节码的方法generateProxyClass
在这里插入图片描述

从划线部分看出,确实是生成了后缀为.class的字节码文件。

写一个工具类,利用上面的方法生成代理对象的字节码并保存下来,并通过反编译查看java源码

用来保存字节码的工具类

public class ProxyUtils {

    public static void generateClassFile(Class clazz,String proxyName){
        //根据类的信息和代理类的名称生成字节码
        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName,
                new Class[]{clazz});
        String paths = clazz.getResource(".").getPath();
        System.out.println(paths);
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(paths+proxyName+".class");
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

在XiaoQiang类后面加上下面两行代码,调用上面的工具类:

ProxyUtils.generateClassFile(afactory.getClass(),staff1.getClass().getSimpleName());
ProxyUtils.generateClassFile(bfactory.getClass(),staff2.getClass().getSimpleName());

最后输出了保存的字节码文件路径,进入目录查看,如下,果然有 P r o x y 1. c l a s s 和 Proxy1.class和 Proxy1.classProxy2.class文件。
在这里插入图片描述

使用jad工具进行反编译,注意反编译之前需要将 P r o x y 1. c l a s s 和 Proxy1.class和 Proxy1.classProxy2.class的名字改一下,不能含有$符号,否则找不到这个文件,使用如下命令进行反编译:
在这里插入图片描述
查看生成的Proxy0.java

import Proxy.AaFactory;
import java.lang.reflect.*;

public final class $Proxy0 extends Proxy
    implements AaFactory
{

    public $Proxy0(InvocationHandler invocationhandler)
    {
        super(invocationhandler);
    }
    //省略一些代码
    //核心业务代码
    public final void saleKouHong(String s)
    {
        try
        {
            super.h.invoke(this, m3, new Object[] {
                s
            });
            return;
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

//省略一些代码
 
}

最重要的是业务方法saleKouHong,可以看出它调用了super.h.invoke(),super指的是父类Proxy,属性h指的是InvocationHandler,而我们自己写的XiaoMeiCompany 实现了 InvocationHandler 接口,并且重写了invoke()方法
在这里插入图片描述

所有最终业务代码其实调用的是XiaoMeiCompany中的Invoke()方法,在这个方法里我们对业务进行了增强(前置方法和后置方法)。这就是动态代理会增强业务的核心原理。
在这里插入图片描述

相关拓展

代理的分类:

1.静态代理
静态代理就是在编译前,代理类就存在了。这种方法很不灵活,实际编程中很少会用到,不多展开。

2.动态代理
在这里插入图片描述

关于动态代理的面试题:

1.Spring事务注解实现的原理

2.为什么MyBatis可以直接使用mapper接口访问数据库

3.Mybatis打开调试模式能够打印sql语句等日志信息,是怎么实现的?实现过程中使用了什么设计 模式

代理的应用:

添加日志切面,添加事务特性,加入缓存代理,系统性能监控,方法拦截,权限控制,流量控制,负载均衡等等。
由于篇幅有限,应用部分不再讲解,更多内容请关注我的公众号【程序员修炼】。

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值