9.3.4 代理模式
9.3.4 代理模式
代理模式是一种应用非常广泛的设计模式,当客户端代码需要调用某个对象时,客户端实际上也不关心是否准确得到该对象,它只要一个能提供该功能的对象即可,此时就可返回该对象的代理(Proxy
)。
在这种设计方式下,系统会为某个对象提供一个代理对象,并由代理对象控制对源对象的引用。代理就是一个Java
对象代表另一个Java
对象来采取行动。在某些情况下,客户端代码不想或不能够直接调用被调用者,代理对象可以在客户和目标对象之间起到中介的作用。
对客户端而言,它不能分辨出代理对象与真实对象的区别,它也无须分辨代理对象和真实对象的区别。客户端代码并不知道真正的被代理对象,客户端代码面向接口编程,它仅仅持有一个被代理对象的接。
总而言之,只要客户端代码不能或不想直接访问被调用对象—这种情况有很多原因,比如需要创建一个系统开销很大的对象,或者被调用对象在远程主机上,或者目标对象的功能还不足以满足需求…而是额外创建一个代理对象返回给客户端使用,那么这种设计方式就是代理模式
程序示例
1 | E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\Proxy |
下面示范一个简单的代理模式,程序首先提供了一个Image
接口,代表大图片对象所实现的接口。
1 | public interface Image |
该接口提供了一个实现类,该实现类模拟了一个大图片对象,该实现类的构造器使用Thread.sleep()
方法来暂停3s
。下面是该BigImage
的程序代码。
1 | // 使用该BigImage模拟一个很大图片 |
上面程序中的粗体字代码暂停了3s,这表明创建一个BigImage
对象需要3s的时间开销程序使用这种延迟来模拟装载此图片所导致的系统开销。如果不采用代理模式,当程序中创建BioiMage
时,系统将会产生3s的延迟。为了避免这种延迟,程序为BigImage
对象提供了一个代理对象, BigImage
类的代理类如下。
1 | public class ImageProxy implements Image |
上面的ImageProxy
代理类实现了与BigImage
相同的show()
方法,这使得客户端代码获取到该代理对象之后,可以将该代理对象当成BigImage
来使用。
在ImageProxy
类的show
方法中增加了控制逻辑,这段控制逻辑用于控制当系统真正调用Image
的show()
时,才会真正创建被代理的BioImage
对象。下面程序需要使用BigImage
对象,但程序并不是直接返回BigImage
实例,而是先返回BigImage
的代理对象,如下面的程序所示:
1 | public class BigImageTest |
上面的程序初始化Image
非常快,因为程序并未真正创建BigImage
对象,只是得到了ImageProxy
代理对象直到程序调用image.show()
方法时,程序需要真正调用BioImage
对象的showO
方法,程序此时才真正创建BigImage
对象。运行上面程序,看到如下所示的结果。
1 | 系统得到Image对象的时间开销:0 |
读者应该能认同:使用代理模式提高了获取Image
对象的系统性能,但可能有读者会提出疑问:程序调用ImageProxy
对象的show()
方法时一样需要创建BigImage
对象,系统开销并未真正减少,只是这种系统开销延迟了而已?
可以从如下两个角度来回答这个问题。
- 把创建
BigImage
推迟到真正需要它时才创建,这样能保证前面程序运行的流畅性,而且能减少BigImage
在内存中的存活时间,从宏观上节省了系统的内存开销。 - 在有些情况下,也许程序永远不会真正调用
ImageProxy
对象的show()
方法—意味着系统根本无须创建BigImage
对象。在这种情形下,使用代理模式可以显著地提高系统运行性能。
使用代理节省开销
第二种情况正是Hibernate
延迟加载所采用的设计模式,相信读者还记得前面介绍Hibernate
关联映射时的知识,当A实体和B实体之间存在关联关系时, Hibernate
默认启用延迟加载,当系统加载A实体时,A实体关联的B实体并未被加载出来,A实体所关联的B实体全部是代理对象——只有等到A实体真正需要访问B实体时,系统才会去数据库里抓取B实体所对应的记录。Hibernate
的延迟加载充分体现了代理模式的优势:当系统加载A实体时,也许只需要访问A实体对应的记录,根本不会访问A的关联实体。如果不采用代理模式,系统需要在加载A实体时,同时加载A实体的所有关联实体,这是很大的系统开销。
代理对象增强目标对象的功能
除了上面出于性能考虑使用代理模式之外,代理模式还有另一种常用场景:当目标对象的功能不足以满足客户端需求时,系统可以为该对象创建一个代理对象,而代理对象可以增强原目标对象的功能。
借助于Java
提供的Proxy
和InvocationHandler
,可以实现在运行时生成动态代理的功能,而动态代理对象就可作为目标对象使用,而且增强了目标对象的功能。
程序示例
1 | E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\DynaProxy\src |
由于**JDK
动态代理只能创建指定接口的动态代理**,所以下面先提供一个Dog
接口,该接口代码非常简单,仅仅在该接口里定义了两个方法。
1 | public interface Dog |
上面接口里只是简单定义了两个方法,并未提供方法实现。下面程序先为该接口提供一个实现类该实现类的实例将会作为被代理的目标对象。下面是该接口实现类的代码。
1 | public class GunDog implements Dog |
上面的代码没有丝毫的特别之处,该Dog
的实现类仅仅为每个方法提供了一个简单实现。现在假设该目标对象(GunDog
)实例的两个方法不能满足实际需要,因此客户端不想直接调用该目标对象。假设客户端需要在GunDog
为两个方法增加事务控制:在目标方法被调用之前开始事务,在目标方法被调用之后结束事务。
为了实现该功能,可以为GunDog
对象创建一个代理对象,该代理对象提供与GunDog
对象相同的方法,而代理对象增强了GunDog
对象的功能。
下面先提供一个TxUtil
类(这个类通常被称为拦截器),该类里包含两个方法,分别用于开始事务、提交事务。下面是TxUtil
类的源代码。
1 | public class TxUtil |
借助于Proxy
和InvocationHandler
就可以实现:当程序调用info()
方法和run()
方法时,系统可以”自动”将beginTx()
和endTx()
两个通用方法插入info()
和run()
方法执行中。JDK
动态代理的关键在于下面的MyInvokationHandler
类,该类是一个InvocationHandler
实现类,该实现类的invoke
方法将会作为代理对象的方法实现。
1 | import java.lang.reflect.*; |
上面的invoke
方法将会作为动态代理对象的所有方法的实现体。上面方法中tx.beginTx();
这行代码调用了开始事务的方法,Object result = method.invoke(target, args);
这行代码通过反射回调了被代理对象的目标方法,tx.endTx();
这行代码调用了结束事务的方法。
通过这种方式,使得代理对象的方法既回调了被代理对象的方法,并为被代理对象的方法增加了事务功能。
下面再为程序提供一个MyProxyFactory
类,该对象专为指定的target
生成动态代理实例。
1 | import java.lang.reflect.*; |
上面的动态代理工厂类提供了一个getProxy()
方法,该方法为target
对象生成一个动态代理对象,这个动态代理对象与target
实现了相同的接口,所以具有相同的public
方法—从这个意义上来看,动态代理对象可以当成target
对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MyInvokationHandler
对象的invoke
方法。例如调用动态代理对象的info()
方法,程序将开始执行invoke()
方法,其执行步骤如下。
- 创建
TXUtil
实例。 - 执行
TxUtil
实例的beginTx()
方法。 - 使用反射以
target
作为调用者执行info()
方法。 - 执行
TxUtil
实例的endTx()
方法。
看到上面的执行过程,读者应该已经发现:使用动态代理对象来代替被代理对象时,代理对象的方法就实现了前面的要求——程序执行info()
和run()
方法时增加事务功能。而且这种方式有一个额外的好处:GunDog
的方法中没有以硬编码的方式调用beginTx()
和endTx()
—这就为系统扩展增加了无限可能性:当系统需要扩展(GunDog
实例的功能时,程序只需要提供额外的拦截器类,并在MyInvokationHandler
的invoke()
方法中回调这些拦截器方法即可。
下面提供一个主程序来测试动态代理的结果。
1 | public class Test |
上面程序中的dog
对象实际上是动态代理对象,只是该动态代理对象也实现了Dog
接口,所以也可以当成Dog
对象使用。程序执行dog
的info()
和run()
方法时,实际上会先执行TxUtil
的beginTx()
方法,再执行target
对象的info()
和run()
方法,最后再执行TxUtil
的endTx()
方法。执行上面的程序,将看到如下所示的结果。
1 | =====模拟开始事务===== |
从运行结果来看,不难发现采用动态代理可以非常灵活地实现解耦。通过使用这种动态代理,程序就为被代理对象增加了额外的功能。
这种动态代理在AOP
(Aspect Orient Program
,面向切面编程)里被称为AOP
代理,AOP
代理可代替目标对象,AOP
代理包含了目标对象的全部方法。但AOP
代理中的方法与目标对象的方法存在差异:AOP
代理里的方法可以在执行目标方法之前、之后插入一些通用处理。AOP
代理所包含的方法与目标对象所包含的方法示意图如图9.6所示。
看到此处,相信读者应该对Spring
的AOP
框架有点感觉了:当Spring
容器中的被代理Bean
实现了一个或多个接口时, Spring
所创建的AOP
代理就是这种动态代理。 Spring AOP
与此示例应用的区别在哪里呢? Spring AOP
更灵活,当Sping
定义InvocationHandler
类的invoke
时,它并没有以硬编码方式决定调用哪些拦截器,而是通过配置文件来决定在invoke
方法中要调用哪些拦截器,这就实现了更彻底的解耦——当程序需要为目标对象扩展新功能时,根本无须改变Java
代理,只需要在配置文件中增加更多的拦截器配置即可。