2014-10-17 78 views
9

我在网上做了一个小型的研究,并回顾了这个网站上的相关主题,但答案是矛盾的:有些人说这是不可能的,其他人说这是可能的,但是很危险。是否可以在没有外部类的情况下序列化匿名类?

目标是传递匿名类的对象作为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

+0

现在你没有机会了。除了非常hardcore的方式,如发送类的字节码到另一边,并在那里恢复 – talex 2014-10-17 10:35:18

回答

6

您可以尝试制作Caller.call() a 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介绍显著的安全问题。你必须考虑类加载器泄漏等问题。

+0

因此,在你的建议中,我们只是避免了使Bar类可序化的必要性,因为调用是静态的? – AdamSkywalker 2014-10-17 13:47:35

+0

@Boosha - 不完全。这是因为在静态方法中声明Foo类有效地使其成为一个静态类。 – 2014-10-17 17:16:37

2

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

+0

我更新了这个问题,以解释为什么我不能有一个顶级的可串行化类。 – AdamSkywalker 2014-10-17 10:31:02

+0

在这种情况下,我不知道,我会在这里留下我的答案,以防其他人在没有限制的情况下出现类似的问题。对不起,我忍不住了。 – Lucas 2014-10-17 10:40:11

3

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

3

你如上所述,因为匿名内部类是类来电中声明在Java中不能工作了,你明确表示无法使用的RPC服务器上该类来电(如果我明白了正确的)例子。请注意,对于Java RPC,只有数据通过网络发送,这些类必须已经在客户端和服务器上可用。尊重你的例子没有意义,因为它看起来像你想发送代码而不是数据。通常,您可以将可序列化的类放入可供服务器和客户端使用的JAR中,并且每个可序列化的类应具有唯一的serialVersionUID。

2

你不能做的正是你想要的东西,这是序列化一个匿名内部类,不也使得其外围实例序列化和序列化过它。这同样适用于本地类。这些不可避免地有具有引用其封闭实例的隐藏字段,所以序列化实例也会尝试序列化它们的封装实例。

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

如果您使用的是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

另外请注意,任何值这个拉姆达捕获(例如,局部变量,或封闭类的静态字段)也必须是可序列化的。

如果您不使用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()); 
    } 
} 

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

对此的一种变化,如果你真的想要使用一个匿名类,就是在静态上下文中实例化它。 (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); 
    } 
} 

虽然这对于静态嵌套类来说并不容易,你仍然必须命名它存储的字段,但仍然无法捕获任何内容,甚至无法将值传递给构造函数。但它确实满足了你最初问题的要求,即如何序列化一个匿名类的实例而不序列化一个封闭的实例。

相关问题