9.3.4 代理模式

9.3.4 代理模式

代理模式是一种应用非常广泛的设计模式,当客户端代码需要调用某个对象时,客户端实际上也不关心是否准确得到该对象,它只要一个能提供该功能的对象即可,此时就可返回该对象的代理(Proxy)。
在这种设计方式下,系统会为某个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个Java对象代表另一个Java对象来采取行动。在某些情况下,客户端代码不想或不能够直接调用被调用者,代理对象可以在客户和目标对象之间起到中介的作用。
对客户端而言,它不能分辨出代理对象与真实对象的区别,它也无须分辨代理对象和真实对象的区别。客户端代码并不知道真正的被代理对象,客户端代码面向接口编程,它仅仅持有一个被代理对象的接。
总而言之,只要客户端代码不能或不想直接访问被调用对象—这种情况有很多原因,比如需要创建一个系统开销很大的对象,或者被调用对象在远程主机上,或者目标对象的功能还不足以满足需求…而是额外创建一个代理对象返回给客户端使用,那么这种设计方式就是代理模式

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Proxy
└─src\
├─BigImage.java
├─BigImageTest.java
├─Image.java
└─ImageProxy.java

下面示范一个简单的代理模式,程序首先提供了一个Image接口,代表大图片对象所实现的接口。

1
2
3
4
public interface Image
{
void show();
}

该接口提供了一个实现类,该实现类模拟了一个大图片对象,该实现类的构造器使用Thread.sleep()方法来暂停3s。下面是该BigImage的程序代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用该BigImage模拟一个很大图片
public class BigImage implements Image
{
public BigImage()
{
try
{
// 程序暂停3s模式模拟系统开销
Thread.sleep(3000);
System.out.println("图片装载成功...");
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
}
// 实现Image里的show()方法
public void show()
{
System.out.println("绘制实际的大图片");
}
}

上面程序中的粗体字代码暂停了3s,这表明创建一个BigImage对象需要3s的时间开销程序使用这种延迟来模拟装载此图片所导致的系统开销。如果不采用代理模式,当程序中创建BioiMage时,系统将会产生3s的延迟。为了避免这种延迟,程序为BigImage对象提供了一个代理对象, BigImage类的代理类如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ImageProxy implements Image
{
// 组合一个image实例,作为被代理的对象
private Image image;
// 使用抽象实体来初始化代理对象
public ImageProxy(Image image)
{
this.image = image;
}
/**
* 重写Image接口的show()方法 该方法用于控制对被代理对象的访问,
* 并根据需要负责创建和删除被代理对象
*/
public void show()
{
// 只有当真正需要调用image的show方法时才创建被代理对象
if (image == null)
{
image = new BigImage();
}
image.show();
}
}

上面的ImageProxy代理类实现了与BigImage相同的show()方法,这使得客户端代码获取到该代理对象之后,可以将该代理对象当成BigImage来使用。
ImageProxy类的show方法中增加了控制逻辑,这段控制逻辑用于控制当系统真正调用Imageshow()时,才会真正创建被代理的BioImage对象。下面程序需要使用BigImage对象,但程序并不是直接返回BigImage实例,而是先返回BigImage的代理对象,如下面的程序所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BigImageTest
{
public static void main(String[] args)
{
long start = System.currentTimeMillis();
// 程序返回一个Image对象,该对象只是BigImage的代理对象
Image image = new ImageProxy(null);
System.out.println(
"系统得到Image对象的时间开销:" + (System.currentTimeMillis() - start));
// 只有当实际调用image代理的show()方法时,程序才会真正创建被代理对象。
image.show();
}
}

上面的程序初始化Image非常快,因为程序并未真正创建BigImage对象,只是得到了ImageProxy代理对象直到程序调用image.show()方法时,程序需要真正调用BioImage对象的showO方法,程序此时才真正创建BigImage对象。运行上面程序,看到如下所示的结果。

1
2
3
系统得到Image对象的时间开销:0
图片装载成功...
绘制实际的大图片

读者应该能认同:使用代理模式提高了获取Image对象的系统性能,但可能有读者会提出疑问:程序调用ImageProxy对象的show()方法时一样需要创建BigImage对象,系统开销并未真正减少,只是这种系统开销延迟了而已?
可以从如下两个角度来回答这个问题。

  1. 把创建BigImage推迟到真正需要它时才创建,这样能保证前面程序运行的流畅性,而且能减少BigImage在内存中的存活时间,从宏观上节省了系统的内存开销。
  2. 在有些情况下,也许程序永远不会真正调用ImageProxy对象的show()方法—意味着系统根本无须创建BigImage对象。在这种情形下,使用代理模式可以显著地提高系统运行性能。

使用代理节省开销

第二种情况正是Hibernate延迟加载所采用的设计模式,相信读者还记得前面介绍Hibernate关联映射时的知识,当A实体和B实体之间存在关联关系时, Hibernate默认启用延迟加载,当系统加载A实体时,A实体关联的B实体并未被加载出来,A实体所关联的B实体全部是代理对象——只有等到A实体真正需要访问B实体时,系统才会去数据库里抓取B实体所对应的记录。
Hibernate的延迟加载充分体现了代理模式的优势:当系统加载A实体时,也许只需要访问A实体对应的记录,根本不会访问A的关联实体。如果不采用代理模式,系统需要在加载A实体时,同时加载A实体的所有关联实体,这是很大的系统开销。

代理对象增强目标对象的功能

除了上面出于性能考虑使用代理模式之外,代理模式还有另一种常用场景:当目标对象的功能不足以满足客户端需求时,系统可以为该对象创建一个代理对象,而代理对象可以增强原目标对象的功能
借助于Java提供的ProxyInvocationHandler,可以实现在运行时生成动态代理的功能,而动态代理对象就可作为目标对象使用,而且增强了目标对象的功能

程序示例

1
2
3
4
5
6
7
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\DynaProxy\src
├─Dog.java
├─GunDog.java
├─MyInvokationHandler.java
├─MyProxyFactory.java
├─Test.java
└─TxUtil.java

由于**JDK动态代理只能创建指定接口的动态代理**,所以下面先提供一个Dog接口,该接口代码非常简单,仅仅在该接口里定义了两个方法。

1
2
3
4
5
6
7
public interface Dog
{
// info()方法声明
void info();
// run()方法声明
void run();
}

上面接口里只是简单定义了两个方法,并未提供方法实现。下面程序先为该接口提供一个实现类该实现类的实例将会作为被代理的目标对象。下面是该接口实现类的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GunDog implements Dog
{
// info方法实现,仅仅打印一个字符串
public void info()
{
System.out.println("我是一只猎狗");
}
// run方法实现,仅仅打印一个字符串
public void run()
{
System.out.println("我奔跑迅速");
}
}

上面的代码没有丝毫的特别之处,该Dog的实现类仅仅为每个方法提供了一个简单实现。现在假设该目标对象(GunDog)实例的两个方法不能满足实际需要,因此客户端不想直接调用该目标对象。假设客户端需要在GunDog为两个方法增加事务控制:在目标方法被调用之前开始事务,在目标方法被调用之后结束事务。
为了实现该功能,可以为GunDog对象创建一个代理对象,该代理对象提供与GunDog对象相同的方法,而代理对象增强了GunDog对象的功能。
下面先提供一个TxUtil类(这个类通常被称为拦截器),该类里包含两个方法,分别用于开始事务、提交事务。下面是TxUtil类的源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TxUtil
{
// 第一个拦截器方法:模拟事务开始
public void beginTx()
{
System.out.println("=====模拟开始事务=====");
}
// 第二个拦截器方法:模拟事务结束
public void endTx()
{
System.out.println("=====模拟结束事务=====");
}
}

借助于ProxyInvocationHandler就可以实现:当程序调用info()方法和run()方法时,系统可以”自动”将beginTx()endTx()两个通用方法插入info()run()方法执行中。
JDK动态代理的关键在于下面的MyInvokationHandler类,该类是一个InvocationHandler实现类,该实现类的invoke方法将会作为代理对象的方法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.*;

public class MyInvokationHandler implements InvocationHandler
{
// 需要被代理的对象
private Object target;
public void setTarget(Object target)
{
this.target = target;
}
// 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Exception
{
TxUtil tx = new TxUtil();
// 执行TxUtil对象中的beginTx()。
tx.beginTx();
// 以target作为主调来执行method方法
Object result = method.invoke(target, args);
// 执行TxUtil对象中的endTx()。
tx.endTx();
return result;
}
}

上面的invoke方法将会作为动态代理对象的所有方法的实现体。上面方法中
tx.beginTx();这行代码调用了开始事务的方法,
Object result = method.invoke(target, args);这行代码通过反射回调了被代理对象的目标方法,
tx.endTx();这行代码调用了结束事务的方法。
通过这种方式,使得代理对象的方法既回调了被代理对象的方法,并为被代理对象的方法增加了事务功能。

下面再为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例。

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

public class MyProxyFactory
{
// 为指定target生成动态代理对象
public static Object getProxy(Object target) throws Exception
{
// 创建一个MyInvokationHandler对象
MyInvokationHandler handler = new MyInvokationHandler();
// 为MyInvokationHandler设置target对象
handler.setTarget(target);
// 创建、并返回一个动态代理
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(), handler);
}
}

上面的动态代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法—从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvokationHandler对象的invoke方法。例如调用动态代理对象的info()方法,程序将开始执行invoke()方法,其执行步骤如下。

  1. 创建TXUtil实例。
  2. 执行TxUtil实例的beginTx()方法。
  3. 使用反射以target作为调用者执行info()方法。
  4. 执行TxUtil实例的endTx()方法。

看到上面的执行过程,读者应该已经发现:使用动态代理对象来代替被代理对象时,代理对象的方法就实现了前面的要求——程序执行info()run()方法时增加事务功能。而且这种方式有一个额外的好处:GunDog的方法中没有以硬编码的方式调用beginTx()endTx()—这就为系统扩展增加了无限可能性:当系统需要扩展(GunDog实例的功能时,程序只需要提供额外的拦截器类,并在MyInvokationHandlerinvoke()方法中回调这些拦截器方法即可。
下面提供一个主程序来测试动态代理的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test
{
public static void main(String[] args) throws Exception
{
// 创建一个原始的GunDog对象,作为target
Dog target = new GunDog();
// 以指定的target来创建动态代理
Dog dog = (Dog) MyProxyFactory.getProxy(target);
// 调用代理对象的info()和run()方法
dog.info();
dog.run();
}
}

上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行doginfo()run()方法时,实际上会先执行TxUtilbeginTx()方法,再执行target对象的info()run()方法,最后再执行TxUtilendTx()方法。执行上面的程序,将看到如下所示的结果。

1
2
3
4
5
6
7
=====模拟开始事务=====
我是一只猎狗
=====模拟结束事务=====
=====模拟开始事务=====
我奔跑迅速
=====模拟结束事务=====

从运行结果来看,不难发现采用动态代理可以非常灵活地实现解耦。通过使用这种动态代理,程序就为被代理对象增加了额外的功能

这种动态代理在AOP(Aspect Orient Program,面向切面编程)里被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理
AOP代理所包含的方法与目标对象所包含的方法示意图如图9.6所示。
这里有一张图片
看到此处,相信读者应该对SpringAOP框架有点感觉了:当Spring容器中的被代理Bean实现了一个或多个接口时, Spring所创建的AOP代理就是这种动态代理。 Spring AOP与此示例应用的区别在哪里呢? Spring AOP更灵活,当Sping定义InvocationHandler类的invoke时,它并没有以硬编码方式决定调用哪些拦截器,而是通过配置文件来决定在invoke方法中要调用哪些拦截器,这就实现了更彻底的解耦——当程序需要为目标对象扩展新功能时,根本无须改变Java代理,只需要在配置文件中增加更多的拦截器配置即可。