在 Java 的 java.lang.reflect 包下提供了一个 Proxy 类和一个 InvocationHandler 接口,通过使用这个类和接口可以生成 JDK 动态代理类或动态代理对象。
使用 Proxy 和 InvocationHandler 创建动态代理
Proxy 提供了用于创建动态代理类和代理对象的静态方法,它也是所有动态代类的父类。如果在程序中为一个或多个接口动态地生成实现类,就可以使用 Proxy 来创建动态代理类;如果需要为一个或多个接口动态地创建实例,也可以使用 Proxy 来创建动态代理实例。
Proxy 提供了如下两个方法来创建动态代理类和动态代理实例。
- static Class<?> getProxyClass(ClassLoader loader, Class<?>...interfaces):创建一个动态代理类所对应的 Class 对象,该代理类将实现 interfaces 所指定的多个接口。第一个 ClassLoader 参数指定生成动态代理类的类加载器。
- static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):直接创建一个动态代理对象,该代理对象的实现类实现了 interfaces 指定的系列接口,执行代理对象的每个方法时都会被替换执行 InvocationHandler 对象的 invoke 方法。
实际上,即使采用第一个方法生成动态代理类之后,如果程序需要通过该代理类来创建对象,依然需要传入一个 InvocationHandler 对象。也就是说,系统生成的每个代理对象都有一个与之关联的 InvocationHandler 对象。
提示:计算机是很“蠢”的,当程序使用反射方式为指定接口生成系列动态代理对象时,这些动态代理对象的实现类实现了一个或多个接口,动态代理对象就需要实现一个或多个接口里定义的所有方法,但问题是:系统怎么知道如何实现这些方法?这个时候就轮到 InvocationHandler 对象登场了——当执行动态代理对象里的方法时,实际上会替换成调用 InvocationHandler 对的 invoke 方法。
程序中可以采用先生成一个动态代理类,然后通过动态代理类来创建代理对象的方式生成一个动态代理对象。代码片段如下:
// 创建一个 InvocationHandler 对象 InvocationHandler handler = new MyInvocationHandler(...); // 使用 Proxy 生成一个动态代理类 proxyClass Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), new Class[]{Foo.class}); // 获取 proxyC1ass 类中带一个 InvocationHandler 参数的构造器 Constructor ctor = proxyClass.getConstructor(new Class[]{InvocationHandler.class}); // 调用 ctor 的 newInstance 方法来创建动态实例 Foo f = (Foo)ctor.newInstance(new Object[]{handler});
上面代码也可以简化成如下代码:
// 创建一个 InvocationHandler 对象 InvocationHandler handler = new MyInvokationHandler(...); // 使用指定的 Proxy 直接生成一个动态代理对象 Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class[] { Foo.class }, handler);
下面程序示范了使用 Proxy 和 InvocationHandler 来生成动态代理对象。
interface Person { void walk(); void sayHello(String name); } class MyInvokationHandler implements InvocationHandler { /* * 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法 其中: proxy:代表动态代理对象 method:代表正在执行的方法 * args:代表调用目标方法时传入的实参。 */ public Object invoke(Object proxy, Method method, Object[] args) { System.out.println("----正在执行的方法:" + method); if (args != null) { System.out.println("下面是执行该方法时传入的实参为:"); for (Object val : args) { System.out.println(val); } } else { System.out.println("调用该方法没有实参!"); } return null; } } public class ProxyTest { public static void main(String[] args) throws Exception { // 创建一个InvocationHandler对象 InvocationHandler handler = new MyInvokationHandler(); // 使用指定的InvocationHandler来生成一个动态代理对象 Person p = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class[] { Person.class }, handler); // 调用动态代理对象的walk()和sayHello()方法 p.walk(); p.sayHello("孙悟空"); } }
上面程序首先提供了一个 Person 接口,该接口中包含了 walk() 和 sayHello() 两个抽象方法,接着定义了一个简单的 InvocationHandler 实现类,定义该实现类时需要重写 invoke() 方法——调用代理对象的所有方法时都会被替换成调用该 invoke() 方法。该 invoke() 方法中的三个参数解释如下。
- proxy:代表动态代理对象。
- method:代表正在执行的方法。
- args:代表调用目标方法时传入的实参。
上面程序中第一行粗体字代码创建了一个 InvocationHandler 对象,第二行粗体字代码根据 InvocationHandler 对象创建了一个动态代理对象。运行上面程序,会看到如下图所示的运行效果。
----正在执行的方法:public abstract void com.jwen.chapter18_5.Person.walk() 调用该方法没有实参! ----正在执行的方法:public abstract void com.jwen.chapter18_5.Person.sayHello(java.lang.String) 下面是执行该方法时传入的实参为: 孙悟空
从上图可以看出,不管程序是执行代理对象的 walk() 方法,还是执行代理对象的 sayHello() 方法,实际上都是执行 InvocationHandler 对象的 invoke() 方法。
看完了上面的示例程序,可能有读者会觉得这个程序没有太大的实用价值,难以理解 Java 动态代理的魅力。实际上,在普通编程过程中,确实无须使用动态代理,但在编写框架或底层基础代码时,动态代理的作用就非常大。
动态代理和AOP
根据前面介绍的 Proxy 和 InvocationHandler,实在很难看出这种动态代理的优势。下面介绍一种更实用的动态代理机制。
开发实际应用的软件系统时,通常会存在相同代码段重复出现的情况,在这种情况下,对于许多刚开始从事软件开发的人而言,他们的做法是:选中那些代码,一路“复制”、“粘贴”,立即实现了系统功能,如果仅仅从软件功能上来看,他们确实己经完成了软件开发。
通过这种“复制”、“粘贴”方式开发出来的软件如下图所示。
采用上图所示结构实现的软件系统,在软件开发期间可能会觉得无所谓,但如果有一大需要修改程序的深色代码的实现,则意味着打开三份源代码进行修改。如果有1个地方甚至1000个地方使用了这段深色代码段,那么修改、维护这段代码的工作量将变成噩梦。
在这种情况下,大部分相有经验的开发者都会将这段深色代码段定义成一个方法,然后让另外三段代码段直接调用该方法即可。在这种方式下,软件系统的结构如下图所示。
对于上图所示的软件系统,如果需要修改深色部分的代码,则只要修改一个地方即可,而调用该方法的代码段,不管有多少个地方调用了该方法,都完全无须任何修改,只要被调用方法被修改了,所有调用该方法的地方就会自然改变——通过这种方式,大大降低了软件后期维护的复杂度。
但采用这种方式来实现代码复用依然产生一个重要问题:代码段1、代码段2、代码段3和深色代码段分离开了,但代码段1、代码段2和代码段3又和一个特定方法耦合了!最理想的效果是:代码块1、代码块2和代码块3既可以执行深色代码部分,又无须在程序中以硬编码方式直接调用深色代码的方法,这时就可以通过动态代理来达到这种效果。
由于 JDK 动态代理只能为接口创建动态代理,所以下面先提供一个 Dog 接口,该接口代码非常简单,仅仅在该接口里定义了两个方法。
public interface Dog { // info方法声明 void info(); // run方法声明 void run(); }
上面接口里只是简单地定义了两个方法,并未提供方法实现。如果直接使用 Proxy 为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。实际情况通常是,软件系统会为该 Dog 接口提供一个或多个实现类,此处先提供一个简单的实现类:GunDog。
public class GunDog implements Dog { // 实现info()方法,仅仅打印一个字符串 public void info() { System.out.println("我是一只猎狗"); } // 实现run()方法,仅仅打印一个字符串 public void run() { System.out.println("我奔跑迅速"); } }
上面代码没有丝毫的特别之处,该 Dog 的实现类仅仅为每个方法提供了一个简单实现。再看需要实现的功能:让代码段1、代码段2和代码段3既可以执行深色代码部分,又无须在程序中以硬编码方式直接调用深色代码的方法。此处假设 info()、run() 两个方法代表代码段1、代码段2,那么要求:程序执行 info()、run() 方法时能调用某个通用方法,但又不想以硬编码方式调用该方法。下面提供一个DogUtil 类,该类里包含两个通用方法。
public class DogUtil { // 第一个拦截器方法 public void method1() { System.out.println("=====模拟第一个通用方法====="); } // 第二个拦截器方法 public void method2() { System.out.println("=====模拟通用方法二====="); } }
借助于 Proxy 和 InvocationHandler 就可以实现——当程序调用 info() 方法和 run() 方法时,系统可以“自动”将 method1() 和 method2() 两个通用方法插入 info() 和 run() 方法中执行。
这个程序的关键在于下面的 MyInvocationHandler 类,该类是一个 InvocationHandler 实现类,该实现类的 invoke() 方法将会作为代理对象的方法实现。
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 { 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 生成动态代理实例。
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 对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行 MyInvocationHandler对象的 invoke() 方法。例如,调用动态代理对象的 info() 方法,程序将开始执行 invoke() 方法,其执行步骤如下。
- 创建 DogUtil 实例。
- 执行 DogUtil 实例的 method1() 方法.
- 使用反射以 target 作为调用者执行 info() 方法。
- 执行 DogUtil 实例的 method2() 方法。
看到上面的执行过程,读者应该已经发现:当使用动态代理对象来代替 target 对象时,代理对象的方法就实现了前面的要求——程序执行 info()、run() 方法时既能“插入”method1()、method2() 通用方法,但 GunDog 的方法中又没有以硬编码方式调用 method1()和 method2()方法。
下面提供一个主程序来测试这种动态代理的效果。
public class Test { 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 Programming,面向切面编程)中被称为 AOP 代理,AOP 代理可代替目标对象,AOP 代理包含了目标对象的全部方法。但 AOP 代理中的方法与目标对象的方法存在差异:AOP 代理里的方法可以在执行目标方法之前、之后插入一些通用处理。
AOP 代理包含的方法与目标对象包含的方法示意图如下图所示。