【问题标题】:JAVA: What are the advantages of an InvocationHandler over an implementation of an Interface?JAVA:InvocationHandler 相对于接口实现的优势是什么?
【发布时间】:2015-12-11 11:33:36
【问题描述】:

今天在课堂上,我们讨论了 Java 编程中的反射。今天课程的一部分是关于在 Java 中使用 InvocationHandler,而不仅仅是实现一个接口。 当我问老师使用调用处理程序有什么好处时,没有一个明确的答案。 假设我们有一个界面 Plugin

public interface Plugin {
    void calculate(double a, double b);
    String getCommand();
}

你可以在一个类Multiply

中轻松实现这个接口
public class Multiply implements Plugin {
    @Override
    public void calculate(double a, double b){
         return a * b;
    }

    @Override
    public String getCommand(){
         return "*";
    }
}

那我为什么更喜欢使用 InvocationHandler 的另一个实现呢?

 public class MyMock {
     public static Object createMock(Class myClass) {
         InvocationHandler handler = new MyInvocationHandler();
         Object result = Proxy.newProxyInstance(myClass.getClassLoader(), new Class[]{myClass}, handler);
         return result;
     }
 }

提前致谢:)

【问题讨论】:

    标签: java interface mocking implementation invocationhandler


    【解决方案1】:

    Proxy 是一个动态 proxy,允许您在运行时改变对象的行为,而不必在编译时决定它

    例如,假设我们想在夜间只返回空值。如果要静态实现它,则需要将逻辑写入所有类中,例如

    if(isNight())
        return null;
    return normalValue;
    

    这要求您实际上可以更改类,并且您需要更改所有类。

    但是,使用Proxy,您可以将上述逻辑写入InvocationHandler,普通类甚至不会知道它们的值在夜间不使用。您的代码现在使用的是动态代理,而不是原来的类,但它不会知道其中的区别。

    这也允许您拥有多个InvocationHandlers,因此您可以使用参数运行您的代码来决定是否要记录调用、出于安全原因阻止调用或任何其他类似的事情,这是完全不可能做到的使用静态实现。


    您不太可能直接使用这些类,因为它们的级别非常低。然而AOP 使用动态代理或字节码操作来完成其任务。如果您曾经使用过 Spring,那么您很可能在不知不觉中使用了 InvocationHandler。当您将@Transactional 放在方法上时,InvocationHandler 将拦截方法调用并为您启动(和结束)事务。

    【讨论】:

    • 鉴于必须编译代理代码,它如何改变运行时的行为?例如,您的意思是主网络服务器可以运行,同时客户端可以创建代理并更改行为,而无需重新编译主网络服务器代码?
    【解决方案2】:

    InvocationHandlerProxy 一起允许在运行时实现接口,而无需编译特定于接口的代码。它通常用于调解对实现相同接口的类的对象的访问。 Proxy 不允许更改现有对象或类的行为。

    例如,它可以用于客户端的远程方法调用,通过网络将方法调用转发到服务器。

    我第一次使用Proxy 是为了记录对表示通过有线格式接收的命令的宽接口的方法调用。这很容易产生非常一致的调试输出,但在界面更改时几乎不需要维护。

    Java 注解接口可以在运行时由Proxy 代理对象表示,以防止类爆炸。

    java.beans.EventHandler 在 lambda 和方法引用出现之前很有用,可以在不膨胀 jar 的情况下实现事件侦听器。

    【讨论】:

      【解决方案3】:

      根据更具体或真实的示例,您可能会更多地使用第三方或开源 API 来使用此类反射。一个非常流行的例子是我的世界,特别是 Bukkit/Spigot。

      此 api 用于编写插件,然后主服务器加载并运行这些插件。这意味着您不能 100% 控制该代码库中存在的某些代码,从而邀请使用反射的解决方案。具体来说,当您想要拦截在 API(甚至是其他插件的 API,例如熟悉的 Vault)中进行的调用时,您可能希望使用Proxy

      我们将继续使用 minecraft 示例,但我们将在此处与 bukkit 的 api 分开(并假装它不接受 PR)。假设 API 的某个部分完全无法按您需要的方式工作。

      public interface Player {
          //This method handles all damage! Hooray!
          public void damagePlayer(Player source, double damage);
      }
      

      这很好,但是如果我们想编写一些代码来发现播放器是否损坏(也许是为了制作很酷的效果?),我们需要修改源代码(分布式插件不可能),或者我们需要找到一种方法来确定何时调用 #damagePlayer 以及使用什么值。所以进来了Proxy

      public class PlayerProxy implements IvocationHandler {
      
          private final Player src;
      
          public PlayerProxy(Player src) {
              this.src = src;
          }
      
          public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
              //Proceed to call the original Player object to adhere to the API
              Object back = m.invoke(this.src, args);
              if (m.getName().equals("damagePlayer") && args.length == 2) {
                  //Add our own effects!
                  //Alternatively, add a hook so you can register multiple things here, and avoid coding directly inside a Proxy
                  if (/* 50% random chance */) {
                      //double damage!
                      args[1] = (double) args[1] * 2;
                      //or perhaps use `source`/args[0] to add to a damage count?
                  }
              }
          }
      }
      

      使用我们的代理,我们有效地创建了一个 fake Player 类,它会简单地调用 Player 的方法。如果我们的PlayerProxy 是用myPlayerProxy.someOtherMethod(...) 调用的,那么它会很高兴地通过反射(上面方法中的m#invoke)传递对myPlayerProxy.src.someOtherMethod(...) 的调用。

      简单地说,您可以根据自己的需要对库中的对象进行烫手:

      //we'll use this to demonstrate "replacing" the player variables inside of the server
      Map<String, Player> players = /* a reflected instance of the server's Player objects, mapped by name. Convenient! */;
      players.replaceAll((name, player) -> 
          (PlayerProxy) Proxy.newProxyInstance(/* class loader */, new Class<?>[]{Player.class}, new PlayerProxy(player)));
      

      InvocationHandler 也可以处理多个接口。通过使用通用 Object 传递调用,您可以在同一个 Proxy 实例中侦听 API 中的各种不同方法。

      【讨论】:

      • 那么它是否需要“damagePlayer”函数来接收自己作为“Player source”参数才能让代理工作?
      • 这就是 Spring 使用注解的原因,所以它可以通过检测注解轻松地用代理包装东西?
      • 基本上在上面的例子中,你有一个“真实”对象,然后是一个复制“代理”对象来包装它。然后,您将“真实”对象与“代理”对象交换,因此所有对原始对象的调用现在都通过代理进行路由。 invoke 方法是 any 方法调用所调用的方法,我们只是挑选我们关心的那些。注释处理有点不同,它们更类似于感兴趣成员的标记。
      猜你喜欢
      • 1970-01-01
      • 2019-02-01
      • 2011-06-17
      • 2019-06-07
      • 1970-01-01
      • 1970-01-01
      • 2013-12-20
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多