利用代理可以在运行时创建一个实现了一组给定接口的新类。
何时使用代理
假设有一个表示接口的 Class 对象(有可能只包含一个接口),它的确切类型在编译时无法知道。这确实有些难度。要想构造一个实现这些接口的类,就需要使用 newInstance 方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。
代理就是来解决上述问题的。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:
- 指定接口所需要的全部方法。
- Object类中的全部方法,例如,toString、equals等。
不能在运行时定义这些方法的新代码。而是要提供一个调用处理器(invocation handler)。调用处理器是实现了 InvocationHandler 接口的类对象。在这个接口中只有一个方法:
Object invoke(Object proxy, Method method, Object[] args)
无论何时调用代理对象的方法,调用处理器的 invoke 方法都会被调用,并向其传递 Method 对象和原始的调用参数(args)。
创建代理对象
创建一个代理对象,需要使用 Proxy 类的 newProxyInstance 方法。这个方法有三个参数:
- 一个类加载器(class loader)。作为 Java 安全模型的一部分,对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。可以用 null 表示使用默认的类加载器,但要保证其类有默认的类加载器。
- 一个 Class 对象数组,每个元素都是需要实现的接口。
- 一个调用处理器。
比如,我们可以定义一个 TraceHander 包装器类存储包装的对象。其中的 invoke 方法打印出被调用方法的名字和参数,随后用包装好的对象作为隐式参数调用这个方法:
/**
* An invocation handler that prints out the method name and parameters, then
* invokes the original method
*/
public class TraceHandler implements InvocationHandler
{
private Object target;
/**
* 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);
}
}
就可以构造用于跟踪方法调用的代理对象:
/**
* This program demonstrates the use of proxies.
*/
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);
// or
// Object proxy = Proxy.newProxyInstance(Comparator.class.getClassLoader(), 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]);
}
}
现在,无论何时用 elements 数组调用某个方法,这个方法的名字和参数就会打印出来,之后再用 value 调用它。
上述程序是使用代理对象对二分查找进行跟踪。这里,首先将用 1~1000 整数的代理填充数组,然后调用 Arrays 类中的 binarySearch 方法在数组中查找一个随机整数。最后,打印出与之匹配的元素。
Integer 类实现了 Comparable 接口。代理对象(即 proxy 变量)属于在运行时定义的类(它有一个名字,如$Proxy0)。这个类也实现了 Comparable 接口。然而,它的 compareTo 方法调用了代理对象处理器的 invoke 方法(也就是调用 compareTo()
时,会调用绑定的对象处理器的 invoke()
)。
下面是程序运行的全部跟踪结果:
500.compareTo(76)
250.compareTo(76)
125.compareTo(76)
62.compareTo(76)
93.compareTo(76)
77.compareTo(76)
69.compareTo(76)
73.compareTo(76)
75.compareTo(76)
76.compareTo(76)
76.toString() // look this
76
代理类的特性
代理类是在程序运行过程中创建的。然而,一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别。
所有的代理类都扩展于 Proxy 类。一个代理类只有一个实例域————调用处理器,它定义在Proxy的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。
所有的代理类都覆盖了 Object 类中的方法 toString、equals 和 hashCode。如同所有的代理方法一样,这些方法仅仅调用了调用处理器的 invoke。Object 类中的其他方法(如 clone 和 getClass)没有被重新定义。
对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次 newProxyInstance 方法的话,那么只能够得到同一个类的两个对象,也可以利用 getProxyClass 方法获得这个类:
Class proxyClass = Proxy.getProxyClass(null, interfaces);
代理类一定是 public 和 final。如果代理类实现的所有接口都是 public,代理类就不属于某个特定的包;否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。