代理模式——Proxy

终于静下心来好好做一下代理模式的笔记了。说实话,代理模式这个词对我来说又熟悉又陌生。你说陌生吧,Spring 的AOP也了解过一点;你说熟悉吧,但总感觉抓不住它的尾巴,滑不溜秋,在脑海里总没有一个确切的概念。拿来《设计模式之禅》一读,还别说,感觉理解那么一点了。所以就七分抄三分悟记一下。

案例展示——Proxy怎么用?

 代理模式可以说是设计模式界的一个明星模式了,连好多不知道设计模式是什么的程序员或许都听过它的名声,不为其他,就因为它代理这个骚气的功能。一听到代理,就联想到了游戏界大名鼎鼎的代练,想不记住都难,所谓真理源于生活而高于生活,所以为了追寻本源,我们还是从生活中去探索该设计模式吧。

 考虑这样一种场景:随着互联网的发展,游戏联网已经成为一种大趋势,无数“英雄豪杰”在网络游戏中大杀四方,仿佛自己就是无所不能的神。可是,无论是现实世界还是虚拟世界,还是分三六九等的,有的人累死累活还是在低级段位挣扎,时间久了,疲了,倦了,于是感觉生命没有了任何意义。熟不知,现实世界虽然没有造物之手干预命运,可虚拟世界却有很多大神翻云覆雨。于是,你看到了向高级段位奋进的曙光,求大神代打。。。代练就这样应运而生了。废话不多说了,直接上设计类图:

image

  • IGamePlayer是一个游戏玩家接口,里面定义了在游戏中常用的功能

  • GamePlayer是一个玩家实现类,实现了IGamePlayer接口

  • GamePlayerProxy是一个GamePlaye的代理类,代理执行GamePlayer的方法

如下是代码实现:

//游戏玩家接口
public interface IGamePlayer {
    //登录游戏
    void login(String user, String password);
    //杀老怪
    void killBoss();
    //升级
    void upgrade();
}

//玩家实现类
public class GamePlayer implements IGamePlayer{
    //玩家的名字
    private String name = "";

    public GamePlayer(String name) {
        this.name = name;
    }
    //登录游戏
    public void login(String user, String password) {
        System.out.println("玩家ID为 " + user + " 的用户正在登录  " + this.name + "登录成功!");
    }
    //杀老怪
    public void killBoss() {
        System.out.println(this.name + " 正在杀老怪!");
    }
    //升级
    public void upgrade() {
        System.out.println(this.name + " 升了一级!");
    }
}

//代理类
public class GamePlayerProxy implements IGamePlayer {
    //持有一个IGamePlayer的引用
    private IGamePlayer gamePlayer = null;
    //通过构造函数传递要对谁代练
    public GamePlayerProxy(IGamePlayer gamePlayer) {
        this.gamePlayer = gamePlayer;
    }
    //代练登录
    public void login(String user, String password) {
        this.gamePlayer.login(user, password);
    }
    //代练杀老怪
    public void killBoss() {
        this.gamePlayer.killBoss();
    }
    //代练升级
    public void upgrade() {
        this.gamePlayer.upgrade();
    }
}

在一个场景中实现得:

public class Client {
    public static void main(String[] args) {
        //定义一个玩家
        IGamePlayer gamePlayer = new GamePlayer("李大海");
        //定义一个代练者
        IGamePlayer proxy = new GamePlayerProxy(gamePlayer);
        //开始打游戏
        System.out.println("游戏开始时间:2018-9-18 10:00");
        //登录
        proxy.login("沧海一声笑", "password");
        //杀怪
        proxy.killBoss();
        //升级
        proxy.upgrade();
        //记录游戏结束时间
        System.out.println("游戏结束时间:2018-9-18 22:00");
    }
}

//结果如下:
游戏开始时间:2018-9-18 10:00
玩家ID为 沧海一声笑 的用户正在登录  李大海登录成功!
李大海 正在杀老怪!
李大海 升了一级!
游戏结束时间:2018-9-18 22:00

深入分析——Proxy是什么?

Proxy的定义

定义: 为其他对象提供一种代理以控制对这个对象的访问。其通用设计类图如下:

image

  • Subject抽象主题类: 抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求

  • RealSubject具体角色: 也叫做被委托角色,被代理角色。是业务逻辑的具体执行者。

  • Proxy代理主题角色: 也叫做委托类,代理类。它负责对真实角色的应用,将抽象类中定义的方法委托给真实角色实现,自己执行。

代理模式的优点
  • 职责清晰: 真实角色就是实现实际的业务逻辑,不用关心其他非本职的事物。

  • 高扩展性: 真实角色可以随时更换或扩展,只需要实现接口就行,而代理不需要有任何变化

Proxy的扩展

代理有普通代理和强制代理之分。

1. 普通代理

 普通代理是指客户端只能访问代理角色,不能访问真实角色。什么意思?就好比说你请了一个律师帮你打官司,任何纠纷都由律师帮你摆平,你只要在幕后就行。下面是设计类图:

image

 延续了前面的案例,只是做了一个很小的改动:分别修改了GamePlayer和GamePlayerProxy的构造函数。在GamePlayer的构造函数中增加一个IGamePlayer的参数,GamePlayerProxy只需要传入被代理者的名字就行。

代码实现如下(只给出改动部分,其他的与上面的案例相同):

//玩家
public class GamePlayer implements IGamePlayer{
    //玩家的名字
    private String name = "";

    //构造函数限制谁能创建对象,同时传递姓名
    public GamePlayer(IGamePlayer gamePlayer, String name) throws Exception {
        if (gamePlayer == null) {
            throw new Exception("不能创建真是角色!");
        } else {
            this.name = name;
        }
    }
    
    //登录游戏
    public void login(String user, String password) {
        System.out.println("玩家ID为 " + user + " 的用户正在登录  " + this.name + "登录成功!");
    }
    //杀老怪
    public void killBoss() {
        System.out.println(this.name + " 正在杀老怪!");
    }
    //升级
    public void upgrade() {
        System.out.println(this.name + " 升了一级!");
    }
}

//代练
public class GamePlayerProxy implements IGamePlayer {
    //持有一个IGamePlayer的引用
    private IGamePlayer gamePlayer = null;
    //通过构造函数传递要对谁代练
    
    public GamePlayerProxy(String name) {
       try {
           gamePlayer = new GamePlayer(this, name);
       } catch (Exception e) {
           e.printStackTrace();
       }
    }
    
    //代练登录
    public void login(String user, String password) {
        this.gamePlayer.login(user, password);
    }
    //代练杀老怪
    public void killBoss() {
        this.gamePlayer.killBoss();
    }
    //代练升级
    public void upgrade() {
        this.gamePlayer.upgrade();
    }
}

//在一个场景中运行
public class Client {
    public static void main(String[] args) {
        //定义一个代练者
        IGamePlayer proxy = new GamePlayerProxy("李大海");
        //开始打游戏
        System.out.println("游戏开始时间:2018-9-18 10:00");
        //登录
        proxy.login("沧海一声笑", "password");
        //杀怪
        proxy.killBoss();
        //升级
        proxy.upgrade();
        //记录游戏结束时间
        System.out.println("游戏结束时间:2018-9-18 22:00");
    }
}

//结果如下:
游戏开始时间:2018-9-18 10:00
玩家ID为 沧海一声笑 的用户正在登录  李大海登录成功!
李大海 正在杀老怪!
李大海 升了一级!
游戏结束时间:2018-9-18 22:00

 我们发现,结果没有任何变化。仅仅修改了构造函数,就屏蔽了真实角色对系统的影响:对于一个代练,我不需要知道真实角色的其他情况,你只要告诉告诉我他的名字就行(登录名和密码)。在该模式下,调用者只知道代理而不用知道真是的角色是谁,屏蔽了真实角色的变更对高层模块的影响,该模式很适合对扩展性要求很高的场合。

2. 强制代理

 强制代理的话和普通代理相反。普通代理是通过代理找到真实角色,而强制代理则是通过真实角色找到自己的代理是谁。就好比说你想和当事人私下和解,不通过他的律师,一打电话约一下,他却说别找他,找他的律师商量,你说气不气人?其实强制代理就是这么回事儿,有事别找我,找我指定的私人代理。下面是设计类图:

image

 在IGamePlayer中增加了一个getProxy()的方法,用于返回每一个玩家的私人代练。该方法需要每个玩家类自己去实现(注意:代练也可以找代练,本例中代练的代理是自己)。

下面是代码实现:

//玩家接口
public interface IGamePlayer {
    //登录游戏
    void login(String user, String password);
    //杀老怪
    void killBoss();
    //升级
    void upgrade();
    //每个人都可以找自己的代理
    IGamePlayer getProxy();
}

//玩家
public class GamePlayer implements IGamePlayer{
    //玩家的名字
    private String name = "";
    //自己的代理
    private IGamePlayer proxy = null;

    public GamePlayer(String name){
            this.name = name;
    }

    //找到自己的代理
    public IGamePlayer getProxy() {
        this.proxy = new GamePlayerProxy(this);
        return this.proxy;
    }

    //登录游戏
    public void login(String user, String password) {
        if (this.isProxy()) {
            System.out.println("玩家ID为 " + user + " 的用户正在登录  " + this.name + "登录成功!");
        } else {
            System.out.println("请使用指定代理访问!");
        }
    }
    //杀老怪
    public void killBoss() {
        if (this.isProxy()) {
            System.out.println(this.name + " 正在杀老怪!");
        } else {
            System.out.println("请使用指定代理访问!");
        }
    }
    //升级
    public void upgrade() {
        if (this.isProxy()) {
            System.out.println(this.name + " 升了一级!");
        } else {
            System.out.println("请使用指定代理访问!");
        }
    }

    //校验是否是代理访问
    private boolean isProxy() {
        if (this.proxy == null) {
            return false;
        } else {
            return true;
        }
    }
}

//代练
public class GamePlayerProxy implements IGamePlayer {
    //持有一个IGamePlayer的引用
    private IGamePlayer gamePlayer = null;

    //通过构造函数传递要对谁代练
    public GamePlayerProxy(IGamePlayer gamePlayer) {
        this.gamePlayer = gamePlayer;
    }

    //代练登录
    public void login(String user, String password) {
        this.gamePlayer.login(user, password);
    }
    //代练杀老怪
    public void killBoss() {
        this.gamePlayer.killBoss();
    }
    //代练升级
    public void upgrade() {
        this.gamePlayer.upgrade();
    }

    //代练的代理是自己
    public IGamePlayer getProxy() {
        return this;
    }
}

//在一个场景中运行:
public class Client {

    /*
    public static void main(String[] args) {
        //定义一个玩家
        IGamePlayer gamePlayer = new GamePlayer("李大海");
        //定义一个代练者
        IGamePlayer proxy = new GamePlayerProxy(gamePlayer);
        //开始打游戏
        System.out.println("游戏开始时间:2018-9-18 10:00");
        //登录
        proxy.login("沧海一声笑", "password");
        //杀怪
        proxy.killBoss();
        //升级
        proxy.upgrade();
        //记录游戏结束时间
        System.out.println("游戏结束时间:2018-9-18 22:00");
    }*/

    public static void main(String[] args) {
        //定义一个玩家
        IGamePlayer gamePlayer = new GamePlayer("李大海");
        //获得指定的代理
        IGamePlayer proxy = gamePlayer.getProxy();
        //开始打游戏
        System.out.println("游戏开始时间:2018-9-18 10:00");
        //登录
        proxy.login("沧海一声笑", "password");
        //杀怪
        proxy.killBoss();
        //升级
        proxy.upgrade();
        //记录游戏结束时间
        System.out.println("游戏结束时间:2018-9-18 22:00");
    }
}

//结果如下(不是自己的私人代练不允许访问):
游戏开始时间:2018-9-18 10:00
请使用指定代理访问!
请使用指定代理访问!
请使用指定代理访问!
游戏结束时间:2018-9-18 22:00

游戏开始时间:2018-9-18 10:00
玩家ID为 沧海一声笑 的用户正在登录  李大海登录成功!
李大海 正在杀老怪!
李大海 升了一级!
游戏结束时间:2018-9-18 22:00

 强制代理的概念就是要从真实角色查找到代理角色,不允许直接访问真实角色。高层模块只要调用getProxy()方法就可以得到真实角色的代理,从而访问自己的所有方法,代理的管理由真实角色自己完成。

3. 个性代理

 代理不但可以实现主题接口,也可以实现其他接口完成不同的任务。就如一个律师不但可以帮你打官司,也可以帮其他人打官司;代练也可以帮许多人代打。代理的目的是在目标对象方法的基础上进行增强,本质通常就是对目标方法的拦截和过滤,例如游戏代理是需要收费的,这个计算功能就是代理的个性,它应该在代理的接口中定义,类图设计如下:

image

代码实现如下(只对修改部分进行展示,其他与案例相同):

//代理接口
public interface Proxy {
    //计算费用
    void count();
}

//代理
public class GamePlayerProxy implements IGamePlayer, Proxy {
    //持有一个IGamePlayer的引用
    private IGamePlayer gamePlayer = null;

    //通过构造函数传递要对谁代练
    public GamePlayerProxy(IGamePlayer gamePlayer) {
        this.gamePlayer = gamePlayer;
    }

    //代练登录
    public void login(String user, String password) {
        this.gamePlayer.login(user, password);
    }
    //代练杀老怪
    public void killBoss() {
        this.gamePlayer.killBoss();
    }
    //代练升级
    public void upgrade() {
        this.gamePlayer.upgrade();
        this.count();
    }

    //计算费用
    public void count() {
        System.out.println("从青铜到王者总费用:3000元");
    }
}

//结果如下:
游戏开始时间:2018-9-18 10:00
玩家ID为 沧海一声笑 的用户正在登录  李大海登录成功!
李大海 正在杀老怪!
李大海 升了一级!
从青铜到王者总费用:3000元
游戏结束时间:2018-9-18 22:00

终极Boss——动态代理

 终于到这个老哥出场了,实话说,前面做的那些都是为这哥们出场做铺垫,不为啥,就是因为这老哥太牛逼,谁让他是很多著名框架的宠儿呢(大名鼎鼎的Spring AOP就是基于动态代理实现的)。

 吹捧了一番动态代理,言归正传说说动态代理是什么。其实前面讲的案例归结起来都需要自己写代理类,所以一般叫做静态代理。而动态代理的话是在实现阶段不需要关心代理谁,在运行阶段会动态生成一个代理类去代理指定的对象,类图设计如下:

image

 增加了一个InvocationHandler接口,该接口是有JDK提供的,所有的动态代理类都需要实现这个接口才能实现JDK的动态代理。GamePlayHandler是一个动态代理的Handler类,负责找到被代理类,并调用被代理类的方法。

下面是代码实现:

//动态代理的Handler类
public class GamePlayerHandler implements InvocationHandler {
    //被代理者
    Class c = null;
    //被代理的实例
    Object obj = null;
    
    //要代理哪个实例
    public GamePlayerHandler(Object obj) {
        this.obj = obj;
    }
    
    //调用被代理类方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(this.obj, args);
        //如果是登录方法,发送一条消息给被代理者,以防被盗号(AOP编程)
        if (method.getName().equalsIgnoreCase("login")) {
            System.out.println("有人在用我的账号登录!");
        }
        return result;
    }
}

//在一个场景中运行:
public class Client {
    public static void main(String[] args) {
        //定义一个玩家
        IGamePlayer gamePlayer = new GamePlayer("李大海");
        //定义一个handler
        InvocationHandler handler = new GamePlayerHandler(gamePlayer);
        //获得类的ClassLoader
        ClassLoader cl = gamePlayer.getClass().getClassLoader();
        //动态产生一个代理者
        IGamePlayer proxy = (IGamePlayer) Proxy.newProxyInstance(cl, new Class[]{IGamePlayer.class}, handler);

        //开始打游戏
        System.out.println("游戏开始时间:2018-9-18 10:00");
        //登录
        proxy.login("沧海一声笑", "password");
        //杀怪
        proxy.killBoss();
        //升级
        proxy.upgrade();
        //记录游戏结束时间
        System.out.println("游戏结束时间:2018-9-18 22:00");
    }
}

//结果如下:
游戏开始时间:2018-9-18 10:00
玩家ID为 沧海一声笑 的用户正在登录  李大海登录成功!
有人在用我的账号登录! //代练登录时发一个通知,防止盗号
李大海 正在杀老怪!
李大海 升了一级!
游戏结束时间:2018-9-18 22:00

 初次接触的时候可能会有点懵,没有创建代理类哪里来的代理啊?别急,其实这就是动态代理的精髓:动态代理类是在知道指定的代理对象时动态创建的(代理对象的类加载器显示加载)。在调试分析时会出现类似$Proxy0这样的结构,如下图:

image

动态代理模型

 前面提了一个关键名词:AOP(面向切面编程),其核心就是动态代理机制。关于AOP是什么,这里就不做拓展。在项目开发中,对于事物,日志,权限等在系统设计阶段可以不用考虑,而在设计后通过AOP的方式横切过去,并不会影响竖直业务的进行。下面是动态代理的模型:

image

其代码实现如下:

//抽象主题
public interface Subject {
    //业务操作
    public void doSomething(String str);
}

//真实主题
public class RealSubject implements Subject {
    //业务处理
    public void doSomething(String str) {
        System.out.println("搞事情。。。" + str);
    }
}

//动态代理的Handler类
public class MyInvocationHandler implements InvocationHandler {
    //被代理的对象
    private Object target = null;
    //通过构造函数传递一个对象
    public MyInvocationHandler(Object obj) {
        this.target = obj;
    }
    //调用被代理类的方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(this.target, args);
    }
}

//动态代理类
public class DynamicProxy<T> {
    public static <T> T newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {
        //找到连接点:方法,属性等
        if (true) {
            //执行一个前置通知
            (new BeforeAdvice()).exec();
        }
        //执行目标并返回结果
        return (T) Proxy.newProxyInstance(loader, interfaces, h);
    }
}

//具体业务的动态代理
public class SubjectDynamicProxy extends DynamicProxy {
    public static <T> T newProxyInstance(Subject subject) {
        //获得ClassLoader
        ClassLoader loader = subject.getClass().getClassLoader();
        //获得接口数组
        Class<?>[] classes = subject.getClass().getInterfaces();
        //获得handler
        InvocationHandler handler = new MyInvocationHandler(subject);
        return newProxyInstance(loader, classes, handler);
    }
}

//通知接口及实现
public interface Advice {
    //执行
    public void exec();
}
//前置通知
public class BeforeAdvice implements Advice {
    public void exec() {
        System.out.println("前置通知被执行。。。");
    }
}

//在一个场景中运行:
public class Client {
    public static void main(String[] args) {
        //定义一个主题
        Subject subject = new RealSubject();
        //定义subject的代理
        Subject proxy = SubjectDynamicProxy.newProxyInstance(subject);
        //代理行为
        proxy.doSomething("开除你!");
    }
}

//结果如下:
前置通知被执行。。。
搞事情。。。开除你!

 看,在执行真正的业务方法之前,插入了一个前置通知方法,永远会在业务方法之前发送一个通知,这就是横切面编程:在不改变我们已有代码结构的情况下增强或控制对象的行为。 这里需要注意一点:实现JDK的动态代理,被代理类必须实现一个接口(其他的动态代理技术如CGLIB可以没有接口)。下面是关于动态代理模型的一个调用时序图:

image

参考

《设计模式之禅》

猜你喜欢

转载自blog.csdn.net/king123456man/article/details/82767576
今日推荐