Steve's Blog

Talk is cheap, show me the code.

0%

动态代理1

代理模式

0. 代理模式

代理模式是一个很常见的设计模式,简单来说就是代理类Proxy替被代理类做一些东西。其中具体又有静态代理和动态代理两种。

1. 静态代理

现在移动支付在中国各个大中小城市都已经非常普及了,平时我们出去吃饭购物都可以使用支付宝或者微信支付轻松完成交易,而不再使用现金。我们现在就以这个作为例子来说明一下代理模式。
有两个角色:微信支付(WeChatPay)和银行(Bank),微信支付本质上就是银行的一个代理。每当你使用微信支付付款时,事实上是微信支付“代理”你和银行进行交互,最终完成了支付。两个类的代码如下:

Bank.java

    public class Bank {
        ...
        public void pay() { //付款功能
            ...
        }
        public void gathering() {   //收款功能
            ...
        }
        ...
    }

WeChatPay.java

    public class WeChatPay {
        ...
        public void pay() { //付款功能
            ...
        }
        public void gathering() {   //收款功能
            ...
        }
        ...
    }

我们发现两个类的方法都是一样的,两个类的本质功能一致,都是对我们的财产进行管理,所以给它们抽象出一个共同的接口:
WealthManagement.java

    public interface WealthManagement {
        public void pay();

        public void gathering();
    }

然后让两个类均实现这个接口。同时由于微信支付是代理了银行的功能,事实上工作的实体是银行,所以一般它需要绑定一个银行类,我们改变后的代码是:
WeChatPay.java

    public class WeChatPay implements WealthManagement {
        ...
        private Bank bank = new Bank();

        public void pay() { //付款功能
            bank.pay();
        }
        public void gathering() {   //收款功能
            bank.gathering();
        }
        ...
    }

一个最简单的微信支付模型出来了,它其中有一个银行类的成员变量,每当需要进行付款或者收款操作时,它都会调用银行的收款和付款方法。
看起来还不错,但是仍然有需要改进之处:
(1) 我们不希望在微信支付中固定某个银行,而是希望用户能在使用微信时手动的添加;
(2) 微信支付调用了银行的功能,我们希望给用户一些提示,告知他们正在使用微信支付代理而非原版的银行功能。
加上之后这两个请求之后,完整的微信支付功能如下:
WeChatPay.java

    public class WeChatPay implements WealthManagement {
        //...
        private Bank bank;

        public WeChatPay(Bank bank) {
            this.bank = bank;
        }

        public void pay() { //付款功能
            System.out.println("您正在使用微信支付...");
            bank.pay();
            System.out.println("微信支付使用完毕...");
        }
        public void gathering() {   //收款功能
            System.out.println("您正在使用微信支付...");
            bank.gathering();
            System.out.println("微信支付使用完毕...");
        }
        //...
    }

我们使用构造方法传入一个银行类变量bank,进行指定具体的银行,并且在微信支付的每个方法中都加入了提醒功能。
完整的银行代码:
Bank.java

    public class Bank implements WealthManagement {
        //...
        public void pay() { //付款功能
            System.out.println("使用银行的付款功能");
        }
        public void gathering() {   //收款功能
            System.out.println("使用银行的收款功能");
        }
        //...
    }

现在貌似可以使用了,我们写一个测试类Test测试一下:
Test.java

    public class Test {
        public static void main(String[] args) {
            Bank construction = new Bank();

            WeChatPay wePay = new WeChatPay(construction);

            wePay.pay();
            System.out.println("----------");
            wePay.gathering();
        }
    }

打印结果:

您正在使用微信支付...
使用银行的付款功能
微信支付使用完毕...
----------
您正在使用微信支付...
使用银行的收款功能
微信支付使用完毕...

这其实就是一个简单的代理模式的例子。被代理类(Bank类)和代理类(WeChatPay类)实现同一个接口,然后代理类(WeChatPay类)持有一个被代理类(Bank类)的成员变量。这样做两个类实现了同样的方法,代理类(WeChatPay类)中的方法会调用自己持有的被代理类(Bank类)对象的方法,相当于是“包装”了一下被代理类的方法。代理类(WeChatPay)做的就是在被代理类(Bank)的基础上加一点工作,它是为被代理类服务的。

这个例子中就是:微信支付WeChatPay代理银行Bank让用户进行财产管理操作,在我们使用微信支付的付款pay方法时,事实上调用的是Bankpay方法,只不过这个过程在微信WeChatPaypay方法的“代理”下完成,微信支付的pay方法对银行的pay方法进行了一下“包装”,在其前后添加了两个提醒的功能。

银行_微信代理

这就是一个静态代理的简单的例子。想说明的一点就是:代理模式就是通过一个代理类来替一个正常工作的类完成额外的一些操作,这些操作对于被代理类来说可以完全不用知道,由代理类完成即可。
例如简单的日志打印模块,我们需要在业务操作之前之后打印相关数据的日志,输入为一个业务操作相关的实例。
我们用代理模式完成,只需要在代理类(日志打印模块)中传入这个实例(业务操作),通过业务操作实例的一些get方法获取一些数据,然后用代理类(日志打印模块)的打印方法打印出这些数据,之后再去调用正常的业务方法即可。

原理分析以及静态代理的问题

静态代理是AOP(Aspect Oriented Programming, 面向切面编程)最简单的实现,想在多个相同实例的方法之前之后都加上点权限管理或者类似的功能,那么使用代理类就可以轻松完成而不用修改代码。

我们针对上面的例子深入分析:
当在代码阶段规定好了代理关系(微信支付代理银行支付,我们需要支付操作时,直接使用微信支付即可)后,微信支付类WeChatPay类通过编译器编译成class文件,当系统运行时,此class已经存在了。这种静态的代理模式固然在访问无法访问的资源,增强现有的接口业务功能方面有很大的优点,但是大量使用这种静态代理,会使我们系统内的类的规模增大,并且不易维护;
并且由于WeChatPayBank的功能 本质上是相同的,WeChatPay只是起到了中介的作用,这种代理在系统中的存在,导致系统结构比较臃肿和松散。
让我们把这个例子补充的更完整一点:对应到现实生活中就是除了使用微信支付进行付款和收款操作之外,我们有时可能还需要进行退款(refund)操作,如果继续使用之前的方法,操作就是:
(1) 在WealthManagement接口中添加refund方法;
(2) 在Bank类中实现refund方法;
(3) 在WeChatPay类中实现refund方法。

实现refund方法的WeChatPay.java

    public class WeChatPay implements WealthManagement {
        //...

        public void refund() { //退款功能
            System.out.println("您正在使用微信支付...");
            bank.refund();
            System.out.println("微信支付使用完毕...");
        }
        //...
    }

如果以后要再实现其它新的方法时,也要重复上述三个步骤。

微信_静态代理局限

我们发现,第(3)步中事实上只是继续在“包装”Bankrefund方法前后继续添加提醒功能。其内容和pay以及gathering方法几乎一样,这样做其实导致业务代码中夹杂了大量的重复的提醒代码,做了大量不必要的重复性工作,同时系统的冗余大量增加。

静态代理代码

提醒代码事实上只有一份就足够了。如何实现呢?
我们希望可以一次性实现这个功能,之后我们不再需要对每个具体的被代理方法实现对应的代理方法。这叫做针对某一类功能实现:例如说提示功能,不需要每个业务方法都有一个对应的带提醒的“包装”方法,而是最好由一个类或者一个方法直接实现所有的提醒功能,类似可以实现的功能还有日志打印。
这时不得不提一个很厉害的工具——动态代理

动态代理正是为了解决代理类中代码爆炸的问题而出现的。具体实现为:在运行状态中,需要代理的地方,根据代理接口和被代理类,动态地创建一个代理类,用完之后,就会销毁,无需我们手动去创建代理类,这样就可以避免了代理类角色的class在系统中代码冗杂的问题了。

2. 动态代理

上述的问题使用动态代理该怎么做呢?我们看代码:
动态代理类NewWeChatPay.java

    public class NewWeChatPay implements InvocationHandler {

        private Object obj;

        public NewWeChatPay(Object obj) {
            this.obj = obj;
        }

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

            System.out.println("您正在使用微信支付...");

            Object result = method.invoke(obj, args);

            System.out.println("微信支付使用完毕...");

            return result;
        }
    }

在这里NewWeChatPay 是一个新的微信支付类,它实现了InvocationHandler接口。这个接口是个调用处理器,每当我们生成的代理类执行方法时,都会调用这个接口实现类的方法。我们先不着急了解它的原理,因为在下一篇文章中会详细阐述。

测试类 DynamicTest.java

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

    public class DynamicTest {
        public static void main(String[] args) throws Throwable {

            Bank construction = new Bank();  //这是一个银行实例

            InvocationHandler handle = new NewWeChatPay(construction); //新建一个微信支付调用处理器

            ClassLoader loader = handle.getClass().getClassLoader(); //获取加载handle的类加载器

            Class[] interfaces = construction.getClass().getInterfaces();  //获取银行实现的接口


            InvocationHandler h = handle;

            Object proxy = Proxy.newProxyInstance(loader, interfaces, h);     //这一句生成了代理银行的微信支付类

            WealthManagement myProxy = (WealthManagement) proxy;

            myProxy.pay();    //微信支付代理类执行pay方法

        }
    }

打印结果:

您正在使用微信支付...
使用银行的付款功能
微信支付使用完毕...

看着很长,但是大家不要怕,其实主要就一句:

Object proxy = Proxy.newProxyInstance(loader, interfaces, h);     //这一句生成了代理银行的微信支付类

调用Proxy类的newProxyInstance()方法生成了一个代理类实例,这个实例就是我们需要的代理类。
我们发现,在使用动态代理之后,的确减少了代码量。之前Bank类的每个方法都需要代理类实现对应的方法,而现在,只需要NewWeChatPay类中的一个invoke方法就可以完成多个方法的代理。
在动态代理中生成的代理类是一个临时的类,我们的代理类需要实现Bank类所实现的接口,所以传入了Bank实现的接口,传入其类加载器是为了确认这些接口的Class类对象和动态代理类的Class对象都是被同一个类加载器加载的,最后的InvocationHandler接口的实例就是我们之前创建好的调用处理器。
动态代理关系

在接下来的一篇文章中,我们会详细探讨Proxy类的newProxyInstance()方法生成临时代理类的过程。

参考文章

[1] Java动态代理机制详解(JDK 和CGLIB,Javassist,ASM)