是否可以在没有外部类的情况下序列化匿名类?

时间:2014-10-17 10:06:13

标签: java serialization rmi anonymous-class

我在网上进行了一项小型研究并审查了该网站上的相关主题,但答案是矛盾的:有些人说这是不可能的,有些人说这是可能的,但很危险。

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

6 个答案:

答案 0 :(得分:7)

您可以尝试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添加远程类的动态类加载会带来严重的安全问题。你必须考虑类加载器泄漏等问题。

答案 1 :(得分:4)

如果你疯狂地做这个技巧,你可以使用反射来查找包含外部类引用的字段并将其设置为null

答案 2 :(得分:3)

如上所述,您的示例无法在Java中工作,因为匿名内部类在类Caller中声明,并且您明确声明类Caller在RPC服务器上不可用(如果我理解正确的话)。请注意,对于Java RPC,仅通过网络发送数据,这些类必须已在客户端和服务器上可用。尊重您的示例并不合理,因为它看起来像您要发送代码而不是数据。通常,您可以在服务器和客户端可用的JAR中使用可序列化类,并且每个可序列化类应具有唯一的serialVersionUID。

答案 3 :(得分:2)

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

答案 4 :(得分: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。)

另请注意,此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());
    }
}

这里的主要区别在于,它缺乏从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);
    }
}

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

答案 5 :(得分:0)

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

Here是一个很好的教程,介绍了如何使用writeObjectreadObject

实现自定义可序列化对象

here是一个很好的教程(网站字体有点让人眼花but乱,但内容值得),它介绍了如何使用Reflection进行序列化。本教程涉及最终字段,但适用于任何字段。

您必须使用反射getDeclaredField