Java核心技术卷I:基础知识(原书第8版):6.5 代理

本文深入介绍了Java SE 1.3引入的代理机制,探讨了如何在运行时创建实现指定接口的新类,解释了代理类的构造过程及特性,并通过示例展示了如何使用代理来跟踪方法调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

铁文整理

6.5 代理

    在本章的最后,讨论一下代理,这是Java SE 1.3新增加的特性。利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。对于应用程序设计人员来说,遇到这种情况的机会很少。如果对这种高级技术不感兴趣,可以跳过本节内容。然而,对于系统程序设计人员来说,代理带来的灵活性却十分重要。

    假设有一个表示接口的Class对象(有可能只包含一个接口),它的确切类型在编译时无法知道,这确实有些难度。要想构造一个实现这些接口的类,就需要使用newInstance方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。

    为了解决这个问题,有些程序将会生成代码,将这些代码放置在一个文件中,调用编译器,然后再加载结果类文件。很自然,这样做的速度会比较慢,并且需要将编译器与程序放在一起。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:

  • 指定接口所需要的全部方法。

  • Object类中的全部方法,例如,toStringequals等。

    然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器。调用处理器是实现了InvocationHandler接口的类对象。在这个接口中只有一个方法:

    Object invoke(Object proxy, Method method, Object[] args)

    无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数。调用处理器必须给出处理调用的方式。

    要想创建一个代理对象,需要使用Proxy类的newProxyInstance方法。这个方法有三个参数:

  • 一个类加载器。作为Java安全模型的一部分,对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。有关类加载器的详细内容将在卷II9章中讨论。目前,用null表示使用默认的类加载器。

  • 一个Class对象数组,每个元素都是需要实现的接口。

  • 一个调用处理器。

    还有两个需要解决的问题。如何定义一个处理器?能够用结果代理对象做些什么?当然,这两个问题的答案取决于打算使用代理机制解决什么问题。使用代理可能出于很多原因,例如:

  • 路由对远程服务器的方法调用。

  • 在程序运行期间,将用户接口事件与动作关联起来。

  • 为调试,跟踪方法调用。

    在列举的示例中,使用代理和调用处理器跟踪方法调用,并且定义了一个TraceHander包装器类存储包装的对象。其中的invoke方法打印出被调用方法的名字和参数,随后用包装好的对象作为隐式参数调用这个方法。

class TraceHandler implements InvocationHandler {

    public TraceHandler(Object t) {

        target = t;

    }

 

    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {

        // print method name and arguments ...

 

        // invoke actual method

        return m.invoke(target, args);

    }

 

    private Object target;

}

    下面说明一下如何构造用于跟踪方法调用的代理对象。

        Object value = ...;

        // construct wrapper

        InvocationHandler handler = new TraceHandler(value);

        // onstruct proxy for one or more interfaces

        Class[] interfaces = new Class[] { Comparable.class };

        Object proxy = Proxy.newProxyInstance(null, interfaces, handler);

    现在,无论何时用proxy调用某个方法,这个方法的名字和参数就会打印出来,之后再用value调用它。

    在例6-7给出的程序中,使用代理对象对二分査找进行跟踪。这里,首先将用11000整数的代理填充数组,然后调用Arrays类中的binarySearch方法在数组中査找一个随机整数,最后,打印出与之匹配的元素。

        Object[] elements = new Object[1000];

 

        // fill elements with proxies for the integers 1 ... 1000

        for (int i = 0; i < elements.length; i++) {

            Integer value = i + 1;

            elements[i] = Proxy.newProxyInstance(...); // proxy for value

        }

 

        // construct a random integer

        Integer key = new Random().nextInt(elements.length) + 1;

 

        // search for the key

        int result = Arrays.binarySearch(elements, key);

 

        // print match if found

        if (result >= 0)

            System.out.println(elements[result]);

    在上述代码中,Integer类实现了Comparable接口。代理对象属于在运行时定义的类(它有一个名字,如$Proxy0)。这个类也实现了Comparable接口。然而,它的compareTo方法调用了代理对象处理器的invoke方法。

    注释:前面已经讲过,在Java SE 5.0中,Integer类实际上实现了Comparable<Integer>。然而,在运行时,所有的泛型类都被取消,代理将它们构造为原Comparable类的类对象。

    binarySearch方法按下面这种方式调用:

        if (elements[i].compareTo(key) < 0) ...

    由于数组中填充了代理对象,所以compareTo调用了TraceHandler类中的invoke方法。这个方法打印出了方法名和参数,之后用包装好的Integer对象调用compareTo

    最后,在示例程序的结尾调用:

        System.out.println(elements[result]);

    println方法调用代理对象的toString,这个调用也会被重定向到调用处理器上,下面是程序运行的全部跟踪结果:……

    可以看出,二分査找算法査找关键字的过程,即每一步都将査找区间缩减一半。注意,即使不属于Comparable接口,toString方法也被代理。在下一节中会看到,有相当一部分的Object方法都被代理。

6-7 ProxyTest.java

import java.lang.reflect.*;

import java.util.*;

 

/**

 * This program demonstrates the use of proxies.

 *

 * @version 1.00 2000-04-13

 * @author Cay Horstmann

 */

public class ProxyTest {

    public static void main(String[] args) {

        Object[] elements = new Object[1000];

 

        // fill elements with proxies for the integers 1 ... 1000

        for (int i = 0; i < elements.length; i++) {

            Integer value = i + 1;

            InvocationHandler handler = new TraceHandler(value);

            Object proxy = Proxy.newProxyInstance(null,

                    new Class[] { Comparable.class }, handler);

            elements[i] = proxy;

        }

 

        // construct a random integer

        Integer key = new Random().nextInt(elements.length) + 1;

 

        // search for the key

        int result = Arrays.binarySearch(elements, key);

 

        // print match if found

        if (result >= 0)

            System.out.println(elements[result]);

    }

}

 

/**

 * An invocation handler that prints out the method name and parameters, then

 * invokes the original method

 */

class TraceHandler implements InvocationHandler {

    /**

     * Constructs a TraceHandler

     *

     * @param t

     *            the implicit parameter of the method call

     */

    public TraceHandler(Object t) {

        target = t;

    }

 

    public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {

        // print implicit argument

        System.out.print(target);

        // print method name

        System.out.print("." + m.getName() + "(");

        // print explicit arguments

        if (args != null) {

            for (int i = 0; i < args.length; i++) {

                System.out.print(args[i]);

                if (i < args.length - 1)

                    System.out.print(", ");

            }

        }

        System.out.println(")");

 

        // invoke actual method

        return m.invoke(target, args);

    }

 

    private Object target;

}

代理类的特性

    现在,我们已经看到了代理类的应用。接下来了解它们的一些特性。需要记住,代理类是在程序运行过程中创建的。然而,一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别。

    所有的代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中,例如,在例6-7给出的程序中,代理Comparable对象时,TraceHandler包装了实际的对象。

    所有的代理类都覆盖了OBject类中的方法toStringequalshashCode,如同所有的代理方法一样,这些方法仅仅调用了调用处理器的invokeObject类中的其他方法(如clonegetClass)没有被重新定义。

    没有定义代理类的名字,Sun虚拟机中的Proxy类将生成一个以字符串$Proxy开头的类名。

    对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法的话,那么只能够得到同一个类的两个对象,也可以利用getProxyClass方法获得这个类:

        Class proxyClass = Proxy.getProxyClass(null, interfaces);

    代理类一定是publicfinal。如果代理类实现的所有接口都是public,代理类就不属于某个特定的包;否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。

    可以通过调用Proxy类中的isProxyClass方法检测一个特定的Class对象是否代表一个代理类。

APIjava.lang.reflect.InvocationHandler 1.3

  • Object invoke(Object proxy, Method method, ObJect[] args):定义了代理对象调用方法时希望执行的动怍。

APIjava.lang.reflect.Proxy 1.3

  • static Class getProxyClass(ClassLoader loader, Class[] interfaces):返回实现指定接口的代理类。

  • static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler handler):构造一个实现指定接口的代理类的实例。所有方法都将调用给定处理器对象的invoke方法。

  • static boolean isProxyClass(Class c):如果c是一个代理类返回true

    到此为止,Java程序设计语言的基础概念介绍完毕了,接口和内部类是两个经常使用的概念。然而,前面已经提到过,代理是一项工具构造者感兴趣的高级技术,对应用程序员来说,并不十分重要。下面准备继续学习由第7章开始介绍的图形和用户接口。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值