【问题标题】:Is it possible to serialize anonymous class without outer class?是否可以在没有外部类的情况下序列化匿名类?
【发布时间】:2014-12-12 21:33:19
【问题描述】:

我在网络上做了一个小研究,并回顾了这个网站上的相关主题,但答案是矛盾的:有人说不可能,有人说有可能,但很危险。

目标是将匿名类的对象作为 RMI 方法的参数传递。由于 RMI 要求,这个类必须是可序列化的。这里没问题,让类Serializable很容易。

但是我们知道内部类的实例持有对外部类的引用(匿名类是内部类)。正因为如此,当我们序列化内部类的实例时,外部类的实例和字段一样被序列化。这就是问题所在:外部类不可序列化,更重要的是——我不想序列化它。我想做的只是发送匿名类的实例。

简单示例 - 这是一个 RMI 服务,其方法接受 Runnable:

public interface RPCService {    
    Object call(SerializableRunnable runnable);
}

这就是我想要调用该方法的方式

void call() {
     myRpcService.call(new SerializableRunnable() {             
         @Override
         public Object run {
             System.out.println("It worked!");
         }
     }        
}

如您所见,我想做的是向另一方发送“动作”——系统 A 描述了应该在系统 B 上运行的代码。这就像在 Java 中发送脚本一样。

如果可能的话,我很容易看到一些危险的后果:例如,如果我们从 Runnable 访问外部类的字段或捕获的最终变量 - 我们会遇到麻烦,因为调用者实例不存在。另一方面,如果我在 Runnable 中使用安全代码(编译器可以检查它),那么我看不到禁止此操作的理由。

因此,如果有人知道,writeObject()readObject() 方法应该如何在匿名类中正确覆盖,或者如何引用外部类 transient,或者解释为什么在 java 中不可能,这将非常有帮助。

UPD 另一个需要考虑的重要事项:外部类不存在于将执行方法(系统 B)的环境中,这就是为什么应该完全排除有关它的信息以避免NoClassDefFoundError

【问题讨论】:

  • 现在你没有机会了。除了非常核心的方式,比如将类的字节码发送到另一端并在那里恢复

标签: java serialization rmi anonymous-class


【解决方案1】:

答案是否定的。你不能这样做,因为内部类需要序列化外部类。当您尝试在内部类中调用外部类的实例方法时,您也会遇到麻烦。你为什么不只是有另一个你可以发送的顶级课程?

【讨论】:

  • 我更新了问题以解释为什么我不能拥有顶级可序列化类。
  • 在这种情况下我不知道,我会在这里留下我的答案,以防其他人会遇到类似的问题而不受您的限制。抱歉,我帮不上忙。
【解决方案2】:

如果你疯了,你可以使用反射来查找包含对外部类的引用的字段并将其设置为null

【讨论】:

    【解决方案3】:

    您可以尝试将Caller.call() 设为static 方法。

    但是,匿名类仍然需要在您反序列化序列化实例的上下文中可用。这是不可避免的。

    (很难想象匿名类可用但封闭类不可用的情况。)


    所以,如果有人可以展示,我如何在我的匿名类中正确覆盖 writeObject 和 readObject 方法...

    如果您将Caller.call() 设为静态,那么我认为您会像命名类一样执行此操作。 (我相信你可以自己找到这方面的例子。)


    确实,(以匿名类可用性问题为模)它有效。在这里,static main 方法替代了static Classer.call() 方法。程序编译运行,表明静态方法中声明的匿名类可以序列化和反序列化。

    import java.io.*;
    
    public class Bar {
    
        private interface Foo extends Runnable, Serializable {}
    
        public static void main (String[] args) 
                throws InterruptedException, IOException, ClassNotFoundException {
    
            Runnable foo = new Foo() {
                @Override
                public void run() {
                    System.out.println("Lala");
                }
            };
    
            Thread t = new Thread(foo);
            t.start();
            t.join();
    
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(foo);
            oos.close();
            Foo foofoo = (Foo) new ObjectInputStream(
                new ByteArrayInputStream(baos.toByteArray())).readObject();
    
            t = new Thread(foofoo);
            t.start();
            t.join();
        }
    }
    

    要记住的另一件重要事情:Caller 类不存在于执行该方法的环境中,因此我想在序列化期间排除有关它的所有信息以避免NoClassDefFoundError

    没有办法避免这种情况。远程 JVM 中的反序列化抱怨的原因是类描述符包含对外部类的引用。即使您设法破坏了引用,并且即使您从未显式或隐式使用反序列化对象中的合成变量,反序列化端也需要解析该引用。

    问题是远程JVM的类加载器在加载内部类的类文件时需要知道外部类的类型。它是验证所必需的。它是反思所需要的。垃圾收集器需要它。

    没有解决方法。

    (我不确定这是否也适用于static 内部类......但我怀疑它确实如此。)


    尝试在没有外部类的情况下序列化匿名 Runnable 实例不仅指序列化问题,还指在另一个环境中执行任意代码的可能性。很高兴看到 JLS 参考,描述这个问题。

    对此没有 JLS 参考。 JLS 中没有指定序列化和类加载器。 (类初始化是......但这是一个不同的问题。)

    可以通过 RMI 在远程系统上运行任意代码。但是,您需要实现 RMI 动态类加载来实现这一点。这是一个参考:

    请注意,将远程类的动态类加载添加到 RMI 会引入重大的安全问题。而且你必须考虑类加载器泄漏等问题。

    【讨论】:

    • 所以,在您的建议中,我们只是避免了使 Bar 类可序列化的必要性,因为调用是静态的?
    • @Boosha - 不完全是。这是因为 Foo 类在静态方法中的声明有效地使其成为静态类。
    【解决方案4】:

    上述示例无法在 Java 中运行,因为匿名内部类是在类 Caller 中声明的,并且您明确指出类 Caller 在 RPC 服务器上不可用(如果我理解正确的话)。请注意,使用 Java RPC,只有数据通过网络发送,类必须已经在客户端和服务器上可用。尊重您的示例没有意义,因为看起来您想要发送代码而不是数据。通常,您会将可序列化的类放在可供服务器和客户端使用的 JAR 中,并且每个可序列化的类都应该有一个唯一的 serialVersionUID。

    【讨论】:

      【解决方案5】:

      你不能完全按照你的意愿去做,即序列化一个匿名内部类,而不使其封闭实例也可序列化和序列化。这同样适用于本地课程。这些不可避免地具有引用其封闭实例的隐藏字段,因此序列化实例也将尝试序列化它们的封闭实例。

      您可以尝试几种不同的方法。

      如果您使用的是 Java 8,则可以使用 lambda 表达式代替匿名内部类。可序列化的 lambda 表达式(不一定)没有对其封闭实例的引用。您只需要确保您的 lambda 表达式不会显式或隐式引用 this,例如使用封闭类的字段或实例方法。代码如下所示:

      public class Caller {
          void call() {
              getRpcService().call(() -> {
                  System.out.println("It worked!");
                  return null;
              });
      }
      

      return null 存在是因为 RPCService.Runnable.run() 被声明为返回 Object。)

      还请注意,此 lambda 捕获的任何值(例如,局部变量或封闭类的静态字段)也必须是可序列化的。

      如果您不使用 Java 8,则下一个最佳选择是使用静态嵌套类。

      public class Caller {
          static class StaticNested implements RPCService.Runnable {
              @Override
              public Object run() {
                  System.out.println("StaticNested worked!");
                  return null;
              }
          }
      
          void call() {
              getRpcService().call(new StaticNested());
          }
      }
      

      这里的主要区别在于它缺乏从call() 方法捕获Caller 的实例字段或局部变量的能力。如有必要,这些可以作为构造函数参数传递。当然,通过这种方式传递的所有东西都必须是可序列化的。

      如果您真的想要使用匿名类,则可以在静态上下文中实例化它。 (See JLS 15.9.2.) 在这种情况下,匿名类将没有封闭实例。代码如下所示:

      public class Caller {
          static RPCService.Runnable staticAnonymous = new RPCService.Runnable() {
              @Override
              public Object run() {
                  System.out.println("staticAnonymous worked!");
                  return null;
              }
          };
      
          void call() {
              getRpcService().call(staticAnonymous);
          }
      }
      

      不过,与静态嵌套类相比,这几乎不会给您带来任何好处。您仍然必须命名它存储的字段,并且您仍然无法捕获任何内容,甚至无法将值传递给构造函数。但它确实满足了您最初的问题,即如何在不序列化封闭实例的情况下序列化匿名类的实例。

      【讨论】:

        【解决方案6】:

        我想添加到这个主题。有一种方法可以实现您想要的,但需要反思。

        Here 是使用writeObjectreadObject 实现自定义可序列化对象的好教程

        here 是一个很好的教程(网站字体有点碍眼,但内容很值得)关于如何使用反射进行序列化。本教程引用了 final 字段,但适用于任何字段。

        你必须使用反射getDeclaredField

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-03-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多