终于静下心来好好做一下代理模式的笔记了。说实话,代理模式这个词对我来说又熟悉又陌生。你说陌生吧,Spring 的AOP也了解过一点;你说熟悉吧,但总感觉抓不住它的尾巴,滑不溜秋,在脑海里总没有一个确切的概念。拿来《设计模式之禅》一读,还别说,感觉理解那么一点了。所以就七分抄三分悟记一下。
代理模式可以说是设计模式界的一个明星模式了,连好多不知道设计模式是什么的程序员或许都听过它的名声,不为其他,就因为它代理这个骚气的功能。一听到代理,就联想到了游戏界大名鼎鼎的代练,想不记住都难,所谓真理源于生活而高于生活,所以为了追寻本源,我们还是从生活中去探索该设计模式吧。
考虑这样一种场景:随着互联网的发展,游戏联网已经成为一种大趋势,无数“英雄豪杰”在网络游戏中大杀四方,仿佛自己就是无所不能的神。可是,无论是现实世界还是虚拟世界,还是分三六九等的,有的人累死累活还是在低级段位挣扎,时间久了,疲了,倦了,于是感觉生命没有了任何意义。熟不知,现实世界虽然没有造物之手干预命运,可虚拟世界却有很多大神翻云覆雨。于是,你看到了向高级段位奋进的曙光,求大神代打。。。代练就这样应运而生了。废话不多说了,直接上设计类图:
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
定义: 为其他对象提供一种代理以控制对这个对象的访问。其通用设计类图如下:
Subject抽象主题类: 抽象主题类可以是抽象类也可以是接口,是一个最普通的业务类型定义,无特殊要求
RealSubject具体角色: 也叫做被委托角色,被代理角色。是业务逻辑的具体执行者。
Proxy代理主题角色: 也叫做委托类,代理类。它负责对真实角色的应用,将抽象类中定义的方法委托给真实角色实现,自己执行。
职责清晰: 真实角色就是实现实际的业务逻辑,不用关心其他非本职的事物。
高扩展性: 真实角色可以随时更换或扩展,只需要实现接口就行,而代理不需要有任何变化
代理有普通代理和强制代理之分。
普通代理是指客户端只能访问代理角色,不能访问真实角色。什么意思?就好比说你请了一个律师帮你打官司,任何纠纷都由律师帮你摆平,你只要在幕后就行。下面是设计类图:
延续了前面的案例,只是做了一个很小的改动:分别修改了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
我们发现,结果没有任何变化。仅仅修改了构造函数,就屏蔽了真实角色对系统的影响:对于一个代练,我不需要知道真实角色的其他情况,你只要告诉告诉我他的名字就行(登录名和密码)。在该模式下,调用者只知道代理而不用知道真是的角色是谁,屏蔽了真实角色的变更对高层模块的影响,该模式很适合对扩展性要求很高的场合。
强制代理的话和普通代理相反。普通代理是通过代理找到真实角色,而强制代理则是通过真实角色找到自己的代理是谁。就好比说你想和当事人私下和解,不通过他的律师,一打电话约一下,他却说别找他,找他的律师商量,你说气不气人?其实强制代理就是这么回事儿,有事别找我,找我指定的私人代理。下面是设计类图:
在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()方法就可以得到真实角色的代理,从而访问自己的所有方法,代理的管理由真实角色自己完成。
代理不但可以实现主题接口,也可以实现其他接口完成不同的任务。就如一个律师不但可以帮你打官司,也可以帮其他人打官司;代练也可以帮许多人代打。代理的目的是在目标对象方法的基础上进行增强,本质通常就是对目标方法的拦截和过滤,例如游戏代理是需要收费的,这个计算功能就是代理的个性,它应该在代理的接口中定义,类图设计如下:
代码实现如下(只对修改部分进行展示,其他与案例相同):
//代理接口
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
终于到这个老哥出场了,实话说,前面做的那些都是为这哥们出场做铺垫,不为啥,就是因为这老哥太牛逼,谁让他是很多著名框架的宠儿呢(大名鼎鼎的Spring AOP就是基于动态代理实现的)。
吹捧了一番动态代理,言归正传说说动态代理是什么。其实前面讲的案例归结起来都需要自己写代理类,所以一般叫做静态代理。而动态代理的话是在实现阶段不需要关心代理谁,在运行阶段会动态生成一个代理类去代理指定的对象,类图设计如下:
增加了一个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这样的结构,如下图:
前面提了一个关键名词:AOP(面向切面编程),其核心就是动态代理机制。关于AOP是什么,这里就不做拓展。在项目开发中,对于事物,日志,权限等在系统设计阶段可以不用考虑,而在设计后通过AOP的方式横切过去,并不会影响竖直业务的进行。下面是动态代理的模型:
其代码实现如下:
//抽象主题
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可以没有接口)。下面是关于动态代理模型的一个调用时序图:
《设计模式之禅》