代理模式
情景引入
比基尼海滩的蟹老板开了家蟹王堡,最开始的时候,材料采购、蟹王堡制作、确认订单、收款都是他一人完成的,随着生意越来越好,他招聘了海绵宝宝做材料采购和厨师,让章鱼哥下单同时兼任财务。为了方便管理,找派大星(bushi)做了一套餐厅的管理系统,在这个系统里面,海绵宝宝需要记录自己采购原材料的一些细节,比如买了什么食材,单价是什么,章鱼哥需要给员工发工资,并把采购相关细节,这样蟹老板一个人时要做的工作分别代理给了海绵宝宝和章鱼哥。这就是代理。
静态代理
系统中的实现如下,就是把原本属于蟹老板的采购行为代理给了海绵宝宝,财务相关行为代理给了章鱼哥,本质上执行者还是蟹老板。
1 | /** |
1 | /** |
1 | /** |
1 | BossPurchaser bossCancer = new BossPurchaser("蟹老板"); |
可以看到如下输出
1 | 海绵宝宝代理执行了purchase方法 |
动态代理
现在问题来了,无论是海绵宝宝采购材料,还是章鱼哥给员工发工资,蟹老板想监控他们的每次操作,即把他们的操作记录为日志, 那在系统里要怎么实现呢?首先想到的就是,无论是海绵宝宝还是章鱼哥进行任何操作,都先执行一个记录当前行为的方法,如上面代码中的System.out.println("代理执行了purchase方法");
,蟹老板这个系统比较小型,我们实现记录方法后,在系统代码里面,遇到员工进行操作时,就加入一行代码调用这个记录操作的方法。但是在一些超级大型的系统里面,记录操作的情景可能有数千种,每种行为之前都加上一行记录操作的代码是不是很麻烦很冗余呢?这时就需要动态代理,这个代理它可以代理海绵宝宝或者章鱼哥的任何行为,并且在执行行为之前记录下操作,这样,海绵宝宝和章鱼哥都不用去记录自己的操作了,而是让动态代理了去实现了这个行为。通过动态代理,使原来海绵宝宝和章鱼哥都需要进行的记录行为操作精简到了动态代理里面,而不用去修改章鱼哥或者海绵宝宝本身。
现在来看看如何使用动态代理实现,接口Purchaser
和类BossPurchaser
,BossPurchaserProxy
可以复用。要使用的类是java.lang.reflect
下提供的Proxy
类和InvocationHandler
接口。
首先创建一个中介类PurchaseInvocationHandler
,它持有被代理对象target,重写了invoke函数,该函数通过反射调用了被代理对象的method方法。在执行method方法之前,我们就可以加入代码记录操作。
1 | import java.lang.reflect.InvocationHandler; |
下面看如何使用这个中介类进行动态代理,中介类持有了被代理对象spongeBob,使用Proxy.newProxyInstance方法创建了代理对象purchaserProxy,然后调用其purchase方法。
1 | Purchaser bossCancer = new BossPurchaser("蟹老板"); |
可以看到如下输出,第一行输出即是动态代理对行为进行的打印
1 | 代理执行了purchase方法 |
动态代理的底层原理
中介类purchaseHandler被创建后可以通过反射来执行被代理的方法,而代理类$Proxy0是在内存中被Proxy.newProxyInstance所创建的,与静态代理不同的是,静态代理的代理类预先写好,而动态代理是在运行中生成的,该代理类持有了中介类,同时该代理类在静态块里面通过反射获得了被代理对象的所有方法。在执行代理对象的方法时,本质上是在使用中介类的invoke方法在执行被代理对象的方法。
动态字节码生成
以上讲到的基于接口的代理模式有四种角色:
- ISubject:被访问资源的抽象,比如采购员接口Purchaser
- SubjectImpl:被访问资源的具体实现类,比如采购员实现BossPurchaser
- SubjectProxy:代理实现类,持有ISubject的具体实例,也实现了接口ISubject,比如BossPurchaserProxy
- Client:访问者,通过代理访问资源的实例
可以看到,无论动态代理还是静态代理,都需要目标对象实现了某个接口,当目标对象没有实现任何接口时,就需要使用动态字节码生成来扩充父类的行为。像静态代理那样为每个目标对象都实现不同的子类来扩展是不现实的。所以需要借助动态字节码生成库CGLIB,在运行期间动态为目标对象生成相应的扩展子类。