corejava11(6.5 代理)

6.5 代理

在本章的最后一节中,我们将讨论代理。可以使用代理在运行时创建实现给定接口集的新类。只有在编译时还不知道需要实现哪些接口时,才需要代理。对于应用程序程序员来说,这不是一个常见的情况,所以如果您对高级向导不感兴趣,可以跳过这一节。但是,对于某些系统编程应用程序,代理提供的灵活性可能非常重要。

6.5.1 何时使用代理

假设您要构造一个类的对象,该类实现一个或多个接口,您可能在编译时不知道这些接口的确切性质。这是个难题。要构造实际的类,可以简单地使用newInstance方法或使用反射来查找构造函数。但是你不能实例化一个接口。您需要在正在运行的程序中定义一个新类。

为了解决这个问题,一些程序生成代码,将其放入一个文件中,调用编译器,然后加载生成的类文件。当然,这是很慢的,而且它还需要与程序一起部署编译器。代理机制是更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类实现您指定的接口。特别是,代理类具有以下方法:

  • 指定接口所需的所有方法;以及
  • Object类中定义的所有方法(toStringequals等)。

但是,您不能在运行时为这些方法定义新的代码。相反,您必须提供一个调用处理程序。调用处理程序是实现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类的toStringequalshashCode方法。与所有代理方法一样,这些方法只是在调用处理程序上调用invokeObject类的其他方法(如clonegetClass)不会被重新定义。

未定义代理类的名称。Oracle虚拟机中的Proxy类生成以字符串$Proxy开头的类名。

对于特定的类加载器和有序的接口集,只有一个代理类。也就是说,如果使用相同的类加载器和接口数组两次调用newProxyInstance方法,则会得到同一类的两个对象。还可以使用getProxyClass方法获取该类:

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

代理类始终是publicfinal。如果代理类实现的所有接口都是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章中处理程序中的异常情况。

猜你喜欢

转载自blog.csdn.net/nbda1121440/article/details/91360166
今日推荐