使用反射生成 JDK 动态代理

在 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,实在很难看出这种动态代理的优势。下面介绍一种更实用的动态代理机制。

开发实际应用的软件系统时,通常会存在相同代码段重复出现的情况,在这种情况下,对于许多刚开始从事软件开发的人而言,他们的做法是:选中那些代码,一路“复制”、“粘贴”,立即实现了系统功能,如果仅仅从软件功能上来看,他们确实己经完成了软件开发。

通过这种“复制”、“粘贴”方式开发出来的软件如下图所示。

使用反射生成 JDK 动态代理

采用上图所示结构实现的软件系统,在软件开发期间可能会觉得无所谓,但如果有一大需要修改程序的深色代码的实现,则意味着打开三份源代码进行修改。如果有1个地方甚至1000个地方使用了这段深色代码段,那么修改、维护这段代码的工作量将变成噩梦。

在这种情况下,大部分相有经验的开发者都会将这段深色代码段定义成一个方法,然后让另外三段代码段直接调用该方法即可。在这种方式下,软件系统的结构如下图所示。

 使用反射生成 JDK 动态代理

对于上图所示的软件系统,如果需要修改深色部分的代码,则只要修改一个地方即可,而调用该方法的代码段,不管有多少个地方调用了该方法,都完全无须任何修改,只要被调用方法被修改了,所有调用该方法的地方就会自然改变——通过这种方式,大大降低了软件后期维护的复杂度。

但采用这种方式来实现代码复用依然产生一个重要问题:代码段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() 方法,其执行步骤如下。

  1. 创建 DogUtil 实例。
  2. 执行 DogUtil 实例的 method1() 方法.
  3. 使用反射以 target 作为调用者执行 info() 方法。
  4. 执行 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 代理包含的方法与目标对象包含的方法示意图如下图所示。

使用反射生成 JDK 动态代理

 

上一篇:java-动态代理与静态代理


下一篇:Java动态代理分析 (含静态代理)