6.5 代理
在本章的最后一节中,我们将讨论代理。可以使用代理在运行时创建实现给定接口集的新类。只有在编译时还不知道需要实现哪些接口时,才需要代理。对于应用程序程序员来说,这不是一个常见的情况,所以如果您对高级向导不感兴趣,可以跳过这一节。但是,对于某些系统编程应用程序,代理提供的灵活性可能非常重要。
6.5.1 何时使用代理
假设您要构造一个类的对象,该类实现一个或多个接口,您可能在编译时不知道这些接口的确切性质。这是个难题。要构造实际的类,可以简单地使用newInstance
方法或使用反射来查找构造函数。但是你不能实例化一个接口。您需要在正在运行的程序中定义一个新类。
为了解决这个问题,一些程序生成代码,将其放入一个文件中,调用编译器,然后加载生成的类文件。当然,这是很慢的,而且它还需要与程序一起部署编译器。代理机制是更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类实现您指定的接口。特别是,代理类具有以下方法:
- 指定接口所需的所有方法;以及
Object
类中定义的所有方法(toString
、equals
等)。
但是,您不能在运行时为这些方法定义新的代码。相反,您必须提供一个调用处理程序。调用处理程序是实现InvocationHandler
接口的任何类的对象。该接口只有一个方法:
Object invoke(Object proxy, Method method, Object[] args)
每当在代理对象上调用一个方法时,调用处理程序的invoke
方法就会被调用,Method
对象和原始调用的参数也是如此。然后调用处理程序必须找出如何处理调用。
6.5.2 创建代理对象
要创建代理对象,请使用Proxy
类的newProxyInstance
方法。该方法有三个参数:
- 类加载器。作为Java安全模型的一部分,不同的类加载器可以用于平台和应用程序类、从Internet下载的类等。我们将在第二卷第9章讨论装载机。在这个例子中,我们指定了加载平台和应用程序类的“系统类加载器”。
Class
对象的数组,每个要实现的接口对应一个。- 调用处理程序。
还有两个问题。我们如何定义处理程序?我们可以对生成的代理对象做什么呢?当然,答案取决于我们想用代理机制解决的问题。代理可以用于多种用途,例如
- 将方法调用路由到远程服务器
- 将用户界面事件与正在运行的程序中的操作关联
- 出于调试目的跟踪方法调用
在我们的示例程序中,我们使用代理和调用处理程序来跟踪方法调用。我们定义了一个TraceHandler
包装类,它存储了一个包装的对象。它的invoke
方法只打印要调用的方法的名称和参数,然后使用包装的对象作为隐式参数调用该方法。
class TraceHandler implements InvocationHandler
{
private Object target;
public TraceHandler(Object t)
{
target = t;
}
public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable
{
// print method name and parameters
. . .
// invoke actual method
return m.invoke(target, args);
}
}
以下是如何构造代理对象,每当调用其方法之一时,该对象都会导致跟踪行为:
Object value = . . .;
// construct wrapper
var handler = new TraceHandler(value);
// construct proxy for one or more interfaces
var interfaces = new Class[] { Comparable.class};
Object proxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[] { Comparable.class } , handler
);
现在,每当在proxy
上调用来自其中一个接口的方法时,都会打印出方法名和参数,然后在value
上调用该方法。
在清单6.10所示的程序中,我们使用代理对象跟踪二进制搜索。我们用整数1 - 1000的代理填充数组。然后我们调用Arrays
类的binarySearch
方法来搜索数组中的随机整数。最后,我们打印匹配元素。
var 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
方法。
注意
正如您在本章前面看到的,Integer类实际上实现了
Comparable<Integer>
。但是,在运行时,所有泛型类型都将被清除,并且代理是用原始Comparable
类的类对象构造的。
binarySearch
方法进行如下调用:
if (elements[i].compareTo(key) < 0) . . .
因为我们用代理对象填充了数组,所以compareTo
调用了TraceHandler
类的invoke
方法。该方法打印方法名和参数,然后在包装的Integer
对象上调用compareTo
。
最后,在示例程序结束时,我们调用
System.out.println(elements[result]);
println
方法在代理对象上调用toString
,并且该调用也被重定向到调用处理程序。
以下是程序运行的完整跟踪:
500.compareTo(288)
250.compareTo(288)
375.compareTo(288)
312.compareTo(288)
281.compareTo(288)
296.compareTo(288)
288.compareTo(288)
288.toString()
您可以通过在每个步骤中将搜索间隔缩短一半来了解二分搜索算法是如何定位键的。注意,toString
方法是代理的,即使它不属于Comparable
接口,正如您将在下一节中看到的那样,某些Object
方法始终是代理的。
清单6.10 proxy/ProxyTest.java
package proxy;
import java.lang.reflect.*;
import java.util.*;
/**
* This program demonstrates the use of proxies.
* @version 1.01 2018-04-10
* @author Cay Horstmann
*/
public class ProxyTest
{
public static void main(String[] args)
{
var 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;
var handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
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
{
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);
}
}
6.5.3 代理类的属性
既然您已经看到了代理类的作用,那么让我们检查一下它们的一些属性。记住,代理类是在运行的程序中动态创建的。但是,一旦创建了它们,它们就是常规类,就像虚拟机中的任何其他类一样。
所有代理类都扩展类Proxy
。一个代理类只有一个实例字段——调用处理程序,它是在Proxy
超类中定义的。执行代理对象任务所需的任何其他数据都必须存储在调用处理程序中。例如,当我们在清单6.10所示的程序中代理Comparable
对象时,TraceHandler
包装了实际的对象。
所有代理类都重写Object
类的toString
、equals
和hashCode
方法。与所有代理方法一样,这些方法只是在调用处理程序上调用invoke
。Object
类的其他方法(如clone
和getClass
)不会被重新定义。
未定义代理类的名称。Oracle虚拟机中的Proxy
类生成以字符串$Proxy
开头的类名。
对于特定的类加载器和有序的接口集,只有一个代理类。也就是说,如果使用相同的类加载器和接口数组两次调用newProxyInstance
方法,则会得到同一类的两个对象。还可以使用getProxyClass
方法获取该类:
Class proxyClass = Proxy.getProxyClass(null, interfaces);
代理类始终是public
和final
。如果代理类实现的所有接口都是public
,则代理类不属于任何特定的包。否则,所有非public
接口必须属于同一个包,代理类也将属于该包。
通过调用Proxy
类的isProxyClass
方法,可以测试特定的Class
对象是否表示代理类。
java.lang.reflect.InvocationHandler
1.3
Object invoke(Object proxy, Method method, Object[] args)
定义此方法以包含您希望在对代理对象调用方法时执行的操作。
java.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<?> cl)
如果cl
是代理类,则返回true
。
最后一章介绍了Java编程语言的面向对象特性。接口、lambda表达式和内部类是您经常会遇到的概念,而克隆、服务加载程序和代理是高级技术,它们主要是库设计者和工具构建者感兴趣的,而不是应用程序程序员。现在您已经准备好学习如何在第7章中处理程序中的异常情况。