【设计模式】第十二篇:车票购买场景中的代理模式讲解
早在 Spring AOP 篇的讲解中,我已经写过关于 AOP 部分是如何用代理模式进行一个处理的,今天相对规范的把这几种方式来整理一下,因为代理模式相对来说代码复杂一点点,所以我们选择先讲解其概念,再使用代码具体演示
一 代理模式的概念
(一) 什么是代理模式
定义:给某个对象提供一个代理对象,用来控制对这个对象的访问
简单的举个例子就是:买火车、飞机票等,我们可以直接从车站售票窗口进行购买,这就是用户直接在官方购买,但是我们很多地方的店铺或者一些路边的亭台中都可以进行火车票的代售,用户直接可以在代售点购票,这些地方就是代理对象
(二) 使用代理对象有什么好处呢?
- 功能提供的这个类 (火车站售票处),可以更加专注于主要功能的实现,比如安排车次以及生产火车票等等
- 代理类 (代售点)可以在功能提供类提供方法的基础上进行增加实现更多的一些功能
这个动态代理的优势,带给我们很多方便,它可以帮助我们实现 无侵入式的代码扩展 ,也就是在不 用修改源码 的基础上,同时 增强方法
补充:缺点就是类的数量,以及复杂度,请求处理速度会相对增大
(三) 分类
代理模式分为静态和动态代理两大种,动态代理又能分为大致两种,所以基本的来说有三种
- ① 静态代理
-
② 动态代理
- ① 基于接口的动态代理 —— JDK 动态代理
- ② 基于子类的动态代理 —— Cglib 动态代理
- ③ javassist 动态代理(这里不做演示)
说明:
静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class
文件就已经存在了。
动态:在程序运行时,运用反射机制动态创建而成
二 代码演示
我们下面演示的背景是来自一个火车票买票的案例,这个案例即,例如买一张800块的火车票,你可以直接在火车站(不考虑现在移动12306等购买,只是例子别较真)买,或者去一个代理点买,但是代理点要赚钱的,所以你买 800 的火车票,你就需要给代理点 1000,收200 手续费,先别管黑不黑心了,我们先看看怎么实现
(一) 传统实现
创建官方售票处接口
public interface RailwayTicketProducer { /** * 售票服务 * @param price */ void saleTicket(float price); }
下面自然就是实现类
public class RailwayTicketProducerImpl implements RailwayTicketProducer { public void saleTicket(float price) { System.out.println("代理销售火车票(扣20%手续费),【官方车站】收到车票钱:" + price); } }
直接调用
public class Test { public static void main(String[] args) { RailwayTicketProducer ticketProducer = new RailwayTicketProducerImpl(); ticketProducer.saleTicket(1000); } }
(二) 静态代理
前面的官方售票处接口和实现类还是一样的,但是代理销售点和官方购票点不一样,其多了一个额外的处理,那就是收 20 % 手续费(真黑啊),所以意味着代理售票点的 售票方法 saleTicket 被增强了
所以我们首先创建一个 代理售票点 ProxyRailwayTicketProducerImpl 类,然后实现官方售票点的接口 RailwayTicketProducer,通过组合引入的方式,将 RailwayTicketProducerImpl 引入,然后在它的 saleTicket 方法中就可以进行额外的业务书写或者说方法增强内容了
比如这里多了一个 price * 0.8f
的简单处理,这样就扣掉了两成的费用
public class ProxyRailwayTicketProducerImpl implements RailwayTicketProducer { private RailwayTicketProducerImpl ticketProducer; public ProxyRailwayTicketProducerImpl() { } public ProxyRailwayTicketProducerImpl(RailwayTicketProducerImpl ticketProducer) { this.ticketProducer = ticketProducer; } @Override public void saleTicket(float price) { ticketProducer.saleTicket(price * 0.8f); } }
测试一下,先 new 一个真实的处理对象,然后创建代理对象,将真实对象传入,再通过代理对象调用 saleTicket ,这样真正调用 saleTicket 的还是原先的 RailwayTicketProducerImpl ,但是被增强的部分也同样被执行了,例如那个 乘以 0.8 的操作
public class Test { RailwayTicketProducerImpl ticketProducer = new RailwayTicketProducerImpl(); // 代理对象 ProxyRailwayTicketProducerImpl proxy = new ProxyRailwayTicketProducerImpl(ticketProducer); proxy.saleTicket(1000); } }
测试结果:
代理销售火车票(扣20%手续费),【官方车站】收到车票钱:800.0
(三) 动态代理(基于接口-jdk)
和前面的传统方式隔着有点远了,这里写完整一下
创建官方售票处(类和接口)
RailwayTicketProducer 接口
public interface RailwayTicketProducer { /** * 售票服务 * @param price */ void saleTicket(float price); }
RailwayTicketProducerImpl 类
实现类中,我们后面只对销售车票方法进行了增强,售后服务并没有涉及到
/** * 生产厂家具体实现 */ public class RailwayTicketProducerImpl implements RailwayTicketProducer{ public void saleTicket(float price) { System.out.println("代理销售火车票(扣20%手续费),【官方车站】收到车票钱:" + price); } }
Client 类
这个类,就是客户类,在其中,通过代理对象,实现购票的需求
首先先来说一下如何创建一个代理对象:答案是 Proxy类中的 newProxyInstance 方法
注意:既然叫做基于接口的动态代理,这就是说被代理的类,也就是文中官方销售车票的类最少必须实现一个接口,这是必要的!
public class Client { public static void main(String[] args) { RailwayTicketProducer producer = new RailwayTicketProducerImpl(); //动态代理 RailwayTicketProducer proxyProduce = (RailwayTicketProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(),new MyInvocationHandler(producer)); //客户通过代理买票 proxyProduce.saleTicket(1000f); } }
newProxyInstance共有 三个参数 来解释一下:
-
ClassLoader:类加载器
- 用于加载代理对象字节码,和被代理对象使用相同的类加载器
-
Class[]:字节码数组
- 为了使被代理对象和的代理对象具有相同的方法,实现相同的接口,可看做固定写法
-
InvocationHandler:如何代理,也就是想要增强的方式
- 也就是说,我们主需要 new 出 InvocationHandler,然后书写其实现类,是否写成匿名内部类可以自己选择
- 如上述代码中 new MyInvocationHandler(producer) 实例化的是我自己编写的一个 MyInvocationHandler类,实际上可以在那里直接 new 出 InvocationHandler,然后重写其方法,其本质也是通过实现 InvocationHandler 的 invoke 方法实现增强
MyInvocationHandler 类
这个 invoke 方法具有拦截的功能,被代理对象的任何方法被执行,都会经过 invoke
public class MyInvocationHandler implements InvocationHandler { private Object implObject ; public MyInvocationHandler (Object implObject){ this.implObject=implObject; } /** * 作用:执行被代理对象的任何接口方法都会经过该方法 * 方法参数的含义 * @param proxy 代理对象的引用 * @param method 当前执行的方法 * @param args 当前执行方法所需的参数 * @return 和被代理对象方法有相同的返回值 * @throws Throwable */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object returnValue = null; //获取方法执行的参数 Float price = (Float)args[0]; //判断是不是指定方法(以售票为例) if ("saleTicket".equals(method.getName())){ returnValue = method.invoke(implObject,price*0.8f); } return returnValue; } }
在此处,我们获取到客户购票的金额,由于我们使用了代理方进行购票,所以代理方会收取一定的手续费,所以用户提交了 1000 元,实际上官方收到的只有800元,这也就是这种代理的实现方式,
运行结果:
代理销售火车票(扣20%手续费),【官方车站】收到车票钱:800.0
(四) 动态代理(基于子类-cglib)
上面方法简单的实现起来也不是很难,但是唯一的标准就是,被代理对象必须提供一个接口,而现在所讲解的这一种就是一种可以直接代理普通 Java 类的方式,同时在演示的时候,我会将代理方法直接以内部类的形式写出,就不单独创建类了,方便大家与上面对照
增加 cglib 依赖坐标
cglib cglib 3.2.4
TicketProducer 类
注意:这里只是一个普通类了
/** * 生产厂家 */ public class TicketProducer { public void saleTicket(float price) { System.out.println("代理销售火车票(扣20%手续费),【官方车站】收到车票钱:" + price); } }
Enhancer 类中的 create 方法就是用来创建代理对象的
而 create 方法又有两个参数
-
Class :字节码
- 指定被代理对象的字节码
-
Callback:提供增强的方法
- 与前面 invoke 作用是基本一致的
- 一般写的都是该接口的子接口实现类:MethodInterceptor
public class Client { public static void main(String[] args) { // 由于下方匿名内部类,需要在此处用final修饰 final TicketProducer ticketProducer = new TicketProducer(); TicketProducer cglibProducer =(TicketProducer) Enhancer.create(ticketProducer.getClass(), new MethodInterceptor() { /** * 前三个三个参数和基于接口的动态代理中invoke方法的参数是一样的 * @param o * @param method * @param objects * @param methodProxy 当前执行方法的代理对象 * @return * @throws Throwable */ public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { Object returnValue = null; //获取方法执行的参数 Float price = (Float)objects[0]; //判断是不是指定方法(以售票为例) if ("saleTicket".equals(method.getName())){ returnValue = method.invoke(ticketProducer,price*0.8f); } return returnValue; } }); cglibProducer.saleTicket(1000f); } }
运行结果:
代理销售火车票(扣20%手续费),【官方车站】收到车票钱:800.0
三 结构图
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法
- 真实主题(Real Subject)类:代理对象所代表的真实对象,是最终要引用的对象,其实现了抽象主题中的具体业务
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能