动态代理(2)----动态代理和AOP
根据前面介绍的Proxy和InvocationHandler,实在很难看出这种动态代理的优势,下面介绍一种更实用的动态代理机制.
只要我们开发一个实际使用的软件系统,总会出现相同代码重复出现的情形,在这种情形下,最常见的做法是:选中那些代码一路“复制”、“粘贴”立即实现系统的功能,如果仅仅从软件功能上来看,他们确实已经完成了软件的开发。对于采用上述方法实现的系统,在软件开发期间可能会觉得无所谓,但如果有一天需要修改程序的公共部分,那意味着打开多份源代码进行修改。如果有100个地方,甚至是1000个地方使用了这段深色代码段,那修改维护这段代码的工作量将变成噩梦。
在这种情况下大部分少有经验的开发这都会将公共代码块定义成一个方法,然后让另外三段代码块直接调用该方法即可。这时如果需要修改公共的代码则只需要修改一次即可。而调用该方法的代码段,不管有多少地方调用了该方法,完全无须任何修改。只要被调用方法被修改了,所有调用该方法的地方自然改变了,通过这种方式大大降低了软件后期维护的复杂度。
但是这种方式最大的问题就是每个调用公共代码块的部分都和公共代码块有耦合,不利于调试和测试。最理想的效果就是公共的代码块既被执行又无须再程序中以硬编码方式直接被调用,这时候就可以通过动态代理的方式来达到这种效果。
由于JDK的动态代理只能创建指定接口的动态代理,所以下面先提供一个Dog接口,该接口代码非常简单,仅仅在该接口里定义了两个方法
interface Dog { // info()方法声明 public void info(); // run()方法声明 public void run(); }
上面接口里只是简单定义了两个方法,并未提供方法实现。如果我们直接使用Proxy为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。在这种情况下,我们将先为该Dog接口提供一个简单的实现类:GunDog
class GunDog implements Dog { // info方法实现,仅仅打印一个字符串 @Override public void info() { System.out.println("我是一只猎狗"); } // run方法实现,仅仅打印一个字符串 @Override public void run() { System.out.println("我奔跑迅速"); } }
上面代码没有丝毫的特别之处,该Dog的实现类仅仅为每个方法提供了一个简单实现。回到开始我们需要实现的功能:让公用的代码不以硬编码的方式出现在需要调用他的类中。此时我们假设info(),run()两个方法代表要调用公共代码的方法,那么要求:程序执行info(),run()方法时能调用某个通用的方法,但又不通过硬编码的方式调用该方法。下面提供DogUtil类该类中包含两个通用的方法。
class DogUtil { // 第一个拦截器方法 public void method1() { System.out.println("-----------模拟第一个通用方法-----------"); } // 第二个拦截器方法 public void method2() { System.out.println("-----------模拟第二个通用方法-----------"); } }
借助于Proxy和InvocationHandler就可以实现:当程序调用info()方法和run()方法时,系统可以“自动”将method1()和method2()两个通用方法插入info()和run()方法执行中。
这个程序的关键在于下面的MyInvokationHandler类,该类是一个InvovationHandler实现类,该实现类的invoke方法将会作为代理的方法实现。
class MyInvokationHandlerPro 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 { DogUtil du = new DogUtil(); // 执行DogUtil对象中method1方法 du.method1(); // 以target作为主调来执行method方法 Object result = method.invoke(target, args); // 执行DogUtil对象中method2方法 du.method2(); return result; } }
上面程序实现了invoke方法时包含了一行关键代码,这行代码通过反射以target作为主调来执行method方法,这就是实现了调用target对象的原有方法。在粗体字代码之前调用DogUtil对象的method1()方法,在其后调用DogUtil对象的method2()方法。
下面为程序提供一个MyProxyFactory类,该对象专为指定的target生成动态代理实例。
class MyProxyFactory { // 为指定target生成动态代理对象 public static Object getProxy(Object target) throws Exception { // 创建一个MyInvokationHandler对象 MyInvokationHandlerPro handler = new MyInvokationHandlerPro(); // 为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、创建DogUtil实例
2、执行DogUtil实例的method1()方法。
3、使用反射以target作为调用者执行info()方法。
4、执行DogUtil实例的method2()方法。
看到上面执行的过程,读者应该已经发现:当我们使用动态代理对象代替target对象时,代理对象的方法就实现类前面的要求:程序执行info()、
run()方法时能调用method1()、method2()通用方法,但GunDog的方法中又没有以硬编码的方式调用method1()和method2()。
下面提供主程序来测试这种动态代理的效果
public class TestProxy { public static void main(String[] args) throws Exception { // 创建一个原始的GunDog对象,作为target Dog target = new GunDog(); // 以指定的target来创建动态代理 Dog dog = (Dog) MyProxyFactory.getProxy(target); dog.info(); dog.run(); } }
上面程序中dog对象实际上是动态代理对象,只是该动态代理对象也实现类Dog接口,所以也可以当成Dog对象使用。程序执行dog的info()和run()方法时,实际上会先执行DogUtil的method1()在执行target对象的info()和run()方法最后在执行DogUtil的method2()执行结果如下图所示
通过上图来看不难发现采用动态代理可以非常灵活的实现解耦。通常而言,当我们使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象来生成动态代理。
这种动态代理在AOP(Aspect Orient Program,面向切面编程)里被称为AOP代理,AOP代理可替代目标对象,AOP代理包含了目标对象的全部发那个发。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前,之后插入一些通用处理。