设计模式之代理(Proxy)模式

序言

  

简介

  代理模式是一种结构型设计模式, 使用后能够为原对象增强其功能以应对不同业务场景,比如说:

  • 隐藏原对象的敏感信息
  • 减少由于大对象极少使用带来的资源消耗
  • 为对象额外提供功能,比如数据缓存,记录日志,服务远程调用功能

应用场景

  代理模式的种类五花八门,根据不同的业务场景代理的叫法也不同。

访问控制(保护代理)

  如果你只希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分, 而客户端则是各种已启动的程序 (包括恶意程序), 此时可使用代理模式。

  代理可仅在客户端凭据满足要求时将请求传递给服务对象。

延迟初始化(虚拟代理)

  如果你有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。

  不在程序启动时创建该对象,而将对象的初始化延迟到真正有需要的时候。

缓存请求结果(缓存代理)

  适用于需要缓存客户请求结果并对缓存生命周期进行管理时,特别是当返回结果的体积非常大时。

  代理可对重复请求所需的相同结果进行缓存,还可使用请求参数作为索引缓存的键值。

本地执行远程服务(远程代理)

  适用于服务对象位于远程服务器上的情形。

  在这种情形中,代理通过网络传递客户端请求,负责处理所有与网络相关的复杂细节。

记录日志请求(日志记录代理)

  适用于当你需要保存对于服务对象的请求历史记录时。

  代理可以在向服务传递请求前进行记录。

实现:Java 的动态代理

  Java 本身就有自己的代理支持,具体在java.lang.reflect包中,利用此包我们可以在运行时动态地创建一个代理类,实现一个或多个接口,并将方法的调用转发到所指定的类。

  由于实际的代理类是在运行时创建的,因此称这个 Java 技术为:动态代理。(具体而言是 JDK 动态代理,因为还有一种动态代理叫做 Cglib 动态代理)

  现在,我们要利用 JDK 动态代理创建一个代理实现(保护代理)。但在这之前,先看一下类图,了解一下动态代理是怎么一回事,它和代理模式的传统定义有一点出入。

  由于 Java 已经创建了Proxy类,所以在使用时我们只需要告诉Proxy类要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy类不是你直接实现的。

  既然这样的代码不能放在Proxy类中,**那么要放在哪里呢?

  放在InvocationHandler接口的实现类中。InvocationHandler是一个接口,里面只有一个invoke()方法,InvocationHandler接口实现类的工作是响应代理的任何调用**。
  下面展示了InvocationHandler接口的源代码:

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

  如何理解该接口呢?
  可以把InvocationHandler接口实现类想成是代理收到方法调用后,请求做实际工作的对象。

  接下来,我们通过一个例子来看看如何使用动态代理吧!

快速入门

  一到大过年的时候,俊男美女李狗蛋张翠花都往村里跑,七大姑八大姨纷纷开始当起了媒人。。。
  假如你就是这些媒人中的一个,负责帮狗蛋他们实现相亲服务系统。
  现在,你有一个好点子,就是在服务系统中加入评分机制,你希望这套系统能帮助你的顾客找到可能的相亲对象。

  如何实现该评分机制呢?(注:这里只是一个简单实现,你可以理解为身高财富颜值的综合分数,请不要纠结它是十分制还是百分制)

  你的服务系统应涉及到一个Person接口,允许设置或取得一个人的信息:

1
2
3
4
5
6
7
8
9
10
11
public interface Person {
String getName();
String getSex();
String getInterests();
int getScore();

void setName(String name);
void setSex(String sex);
void setInterests(String interests);
void setScore(int score);
}

  接着,让PersonImpl类实现该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class PersonImpl implements Person {
private String name;
private String sex;
private String interests;
// 评分
private int score;
// 评分人数
private int scoreCount = 0;

@Override
public String getName() {
return name;
}

@Override
public void setName(String name) {
this.name = name;
}

@Override
public String getSex() {
return sex;
}

@Override
public void setSex(String sex) {
this.sex = sex;
}

@Override
public String getInterests() {
return interests;
}

@Override
public void setInterests(String interests) {
this.interests = interests;
}

@Override
public int getScore() {
if (scoreCount == 0) return 0;
return (score / scoreCount);
}

@Override
public void setScore(int score) {
this.score += score;
scoreCount++;
}
}

  但现在的系统有点问题,顾客投诉啦!
  李狗蛋用过该系统后,抱怨道:“我单身狗当了二十多年,一直以为是我的原因,但是我后来才发现原来有人篡改了我的颜值信息,把我变丑了,我明明很帅的好嘛!我还发现居然有无耻之徒故意给自己评高分,来拉高自己的 score!我觉得系统不应该允许用户篡改别人的颜值信息,也不应该允许用户给自己打分数。”
我李狗蛋不服

  虽然我们有点怀疑李狗蛋找不到女朋友可能是其他的原因。。但是李狗蛋说的没错,系统不应该允许用户篡改别人的数据。
  但是,对目前系统中我们定义的Person类而言,任何客户都可以调用任何方法。

  这是一个我们可以使用保护代理的绝佳例子。

  什么是保护代理?

  这是一种根据访问权限决定客户可否访问对象的代理。比方说,你是老板,手下很多员工,保护代理允许普通员工调用对象上的某些方法,经理还可以多调用一些其他的方法,而人力资源处的雇员可以调用对象上的所有方法。

  在我们的系统中,我们希望:

  • 用户可以设置自己的信息,同时又防止他人更改这些信息
  • 评分机制则相反,你不能更改自己的评分,但是他人却可以设置你的评分。

  在Person中,已经有许多getter方法了,但每个方法的返回信息都是公开的,任何用户都可以调用他们。

  因此,现在,我们有一些问题要修正:用户不可以设置自己的评分,也不可以改变其他用户的个人信息。

  要修正这些问题,你必须创建两个代理:一个访问你自己的Person对象,另一个访问另一个用户的Person对象。这样,代理就可以控制在每一种情况下允许哪一种请求。

  创建这种代理,我们必须使用 Java API 的动态代理。Java 会为我们创建两个代理,而我们只需要提供 handler 来处理代理转来的方法。

① 创建两个实现 InvocationHandler 接口的类

  现在,需要创建两个实现了InvocationHandlerd(调用处理器)接口的实现类,其中一个给拥有者使用,另一个给非拥有者使用。
  究竟什么是InvocationHandler呢?

  我们可以这么理解:当代理的方法被调用时,代理就会把这个调用转发给InvocationHandler的实现类,该类的invoke()方法一定会被调用。

  InvocationHandler的接口代码如下:

1
2
3
4
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

  这里只有一个名为invoke()的方法,不管代理被调用的是何种方法,处理器被调用的一定是invoke()方法。让我们看看这是如何工作的。

现在我们开始创建客户本人调用处理器和他人调用处理器(注意这不是代理类哦)。

  当proxy调用invoke时,要如何做?

  通常,会先检查该方法是否来自proxy,并基于该方法的名称和变量做决定。

  现在,我们来实现OwnInvocationHandler类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

// 用户本人的调用处理器
public class OwnInvocationHandler implements InvocationHandler {

private Person person;

public OwnInvocationHandler(Person person) {
// 将 person 传入构造器,并保持它的引用
this.person = person;
}

// 每次 proxy 方法被调用,都会调用该方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {

try {
// 若方法名首个单词为 get,可以执行该方法,客户当然可以查看自己的任何信息
if (method.getName().startsWith("get")) {
return method.invoke(person, args);
} //否则若方法名为 setScore,则抛出一个异常,代表客户不能给自己打分
else if (method.getName().equals("setScore")) {
throw new IllegalAccessException();
} // 若方法名首个单词为 set,可以执行该方法。客户可以执行除了给自己打分外其他所有的设置自身的方法
else if (method.getName().startsWith("set")) {
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 若调用其他的方法,一律返回 null
return null;
}
}

  对于他人代理类,跟上面的代码异曲同工,只需要把setScore()方法和get的方法对他人开放,其他set方法抛异常即可,这里不做演示,读者可以自己实现。

② 利用代理类并实例化代理对象

  现在,只剩下创建动态Proxy类,并实例化Proxy对象了。让我们开始编写一个以Person为参数,并知道如何为Person对象创建客户本人对象代理的方法。也就是说,我们要创建一个代理,将它的方法调用转发再给OwnInvocationHandler接口实现类。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.lang.reflect.Proxy;

public class OwnProxy {
private Person person;

public OwnProxy(Person person) {
this.person = person;
}

// 获得客户本人的代理
public Person getOwnProxy(){
return (Person)Proxy.newProxyInstance(
person.getClass().getClassLoader(), // 获取类加载器
person.getClass().getInterfaces(), // 获取类的接口
new OwnInvocationHandler(person)); // 获取调用处理器
}
}

  OwnProxy类中只有一个方法,那就是获得客户本人的代理对象。
  其中,Proxy.newProxyInstance()的方法参数:

  • **第一个参数需要Person对象(由于多态所以为PersonImpl对象)的类载入器,因为代理类是在运行期才被创建出来的,我们需要将它加入 JVM 内存创建该对象;
  • 第二个参数需要Person对象的接口,因为要根据接口信息来拦截特定的方法;
  • 第三个就是调用处理集啦,因为代理类会将它的方法调用转发给OwnInvocationHandler来处理,嗯,这个我们强调好几次了。**

  可能有很多人把InvocationHandler当做了代理,但它根本就不是proxy,它只是一个帮助proxy的类,proxy会把调用转发给它处理Proxy类本身是利用静态的Proxy.newProxyInstance()方法在运行时动态创建的,它是Java API自带的,使得Java程序员根据这种规则去创建代理类。

  此处笔者将InvocationHandler接口实现类和代理类分开来写是为了让读者明白上面反复提到的那一点:InvocationHandler接口实现类不是代理类!你理解了这个概念就好了,很重要。
  现在,为了代码的简洁,我们把它们两个合成一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 该类可以获取客户本人的代理,也实现了 InvocationHandler 接口,所以能同时帮助代理对象处理其请求方法
public class OwnProxyInvocationHandler implements InvocationHandler {

// 要被代理的对象,当然也可以写 Object 对象,它是父类,所以在构造器里可以传入任何子类
private Person person;

public OwnProxyInvocationHandler(Person person) {
this.person = person;
}

// 获得客户本人的代理对象
public Person getOwnProxy() {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(), // 获取类加载器
person.getClass().getInterfaces(), // 获取类的接口
this); //获取调用处理器,该类实现了InvocationHandler接口,调用自身即可
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
try {
// 若方法名首个单词为get,可以执行该方法,客户当然可以查看自己的任何信息
if (method.getName().startsWith("get")) {
return method.invoke(person,args);
} // 否则若方法名为setScore,则抛出一个异常,代表客户不能给自己打分
else if (method.getName().equals("setScore")) {
throw new IllegalAccessException();
} // 若方法名首个单词为set,可以执行该方法。客户可以执行除了给自己打分外其他所有的设置自身的方法
else if (method.getName().startsWith("set")) {
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// 若调用其他的方法,一律返回null
return null;
}
}

  该类不仅可以创建代理对象,还实现了InvocationHandler接口,具体解释请见注释。好了,就差最后一步了,我们创建测试类并实例化对象来测试吧!

③ 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {
public static void main(String[] args) {
// 1.创建被代理对象
Person person = new PersonImpl();
// 2.将被代理对象作为参数传入
OwnProxyInvocationHandler ownProxy = new OwnProxyInvocationHandler(person);
// 3.获取代理对象
Person proxy = ownProxy.getOwnProxy();
// 4.代理对象设置方法(调用时都会转发给invoke方法处理在返回结果)
proxy.setName("李狗蛋");//客户本人代理对象可以设置自己的名字
proxy.setScore(8);//客户本人代理对象不可以给自己打分,执行到此时会抛出异常
proxy.setSex("男");
}
}

  我们的客户本人的代理只能设置自己的姓名,性别等信息,但是若想设置自己的分数,就会抛出一个异常,如下图所示:

  当然,真实设计的系统肯定不是抛出一个异常,而是别的效果。比如说,用户查看自己信息界面的时候,有很多个修改按钮可以点击,但修改分数按钮是灰色的。

优与劣

优点

  • 可以在客户端毫无察觉的情况下控制服务对象
  • 如果客户端对服务对象的生命周期没有特殊要求, 可以对生命周期进行管理
  • 即使服务对象还未准备好或不存在, 代理也可以正常工作
  • 遵循开闭原则。 你可以在不对服务或客户端做出修改的情况下创建新代理

缺点

  • 服务响应可能会延迟
  • 代码可能会变得复杂, 因为需要新建许多类

参考

  • Eric Freeman. HeadFirst 设计模式 [M]. 中国电力出版社,2007

文章信息

时间 说明
2019-05-07 初版
2022-01-13 重构
0%