如何安全地序列化一个lambda函数?

9
尽管Java 8中可以序列化lambda表达式,但这是强烈不建议的,甚至序列化内部类也不鼓励。给出的原因是lambda表达式在另一个JRE上可能无法反序列化。然而,这是否意味着有一种安全地序列化lambda的方法呢?
例如,假设我定义一个类如下:
public class MyClass {
    private String value;
    private Predicate<String> validateValue;

    public MyClass(String value, Predicate<String> validate) {
        this.value = value;
        this.validateValue = validate;
    }

    public void setValue(String value) {
        if (!validateValue(value)) throw new IllegalArgumentException();
        this.value = value;
    }

    public void setValidation(Predicate<String> validate) {
        this.validateValue = validate;
    }
}

如果我像这样声明一个类的实例,那么它不应该被序列化:
MyClass obj = new MyClass("some value", (s) -> !s.isEmpty());

但如果我像这样创建了一个类的实例:

// Could even be a static nested class
public class IsNonEmpty implements Predicate<String>, Serializable {
    @Override
    public boolean test(String s) {
        return !s.isEmpty();
    }
}

MyClass isThisSafeToSerialize = new MyClass("some string", new IsNonEmpty());

现在序列化这个对象安全吗?我的直觉告诉我是安全的,因为在java.util.function中的接口没有理由被视为与其他随机接口不同。但我仍然有所顾虑。


1
接口对于序列化来说完全无关,因此实现“Predicate”与实现任何其他接口的影响相同,即没有。但是,您认为Lambda在另一个JRE上可能无法正确反序列化的假设是错误的。它们具有明确定义的持久表示 - Holger
@Holger那为什么oracle文档似乎表明它们不支持呢? - Justin
1
好的,它包含了与序列化相关的内部类问题的参考链接(https://docs.oracle.com/javase/tutorial/java/javaOO/nested.html#serialization)。简而言之,这可能会创建编译器依赖性,而不是JRE特定的问题。当然,文本有点误导人。请注意,存在意外序列化周围上下文的捕获值(包括`this`)的危险... - Holger
1个回答

15

这取决于您想要哪种类型的安全性。序列化的 lambda 表达式是可以在不同的 JRE 之间共享的,它们具有明确定义的持久表示,即 SerializedLambda。当您学习其工作原理时,您会发现它依赖于定义类的存在,该类将具有重构 lambda 表达式的特殊方法。

使其不可靠的是编译器特定工件的依赖关系,例如合成目标方法,其具有一些生成名称,因此简单的更改,如插入另一个 lambda 表达式或使用不同的编译器重新编译类,都可能破坏与现有序列化 lambda 表达式的兼容性。

然而,使用手动编写的类也无法免疫此问题。如果没有显式声明的 serialVersionUID,默认算法将通过哈希类工件(包括私有和合成工件)来计算 ID,并添加类似的编译器依赖性。因此,如果您想要可靠的持久形式,则最小要做的是声明显式的 serialVersionUID

或者您可以采用最强大的形式:

public enum IsNonEmpty implements Predicate<String> {
    INSTANCE;

    @Override
    public boolean test(String s) {
        return !s.isEmpty();
    }
}

对该常量进行序列化不会存储实际实现的任何属性,除了它的类名(当然还有它是一个enum)和对该常量名称的引用。在反序列化时,将使用该名称的实际唯一实例。


请注意,可序列化的 lambda 表达式可能会创建安全问题,因为它们打开了一种获取对象并调用目标方法的替代方式。然而,这适用于所有可序列化的类,正如您提出问题和此答案中显示的所有变体都允许有意地反序列化对象以允许调用封装的操作。但是对于显式可序列化的类,作者通常更加意识到这一点。


我认为枚举类型本身并不会带来任何安全隐患;当进行序列化时,它基本上只是类名和实例名。这怎么可能成为一个安全问题呢?问题的关键在于攻击者能否在我没有意识到的情况下访问到“INSTANCE”变量。 - Justin
2
没错。将一个类序列化就像添加一个额外的public构造函数(或访问器),即使类本身不是public也可以使用。与像Predicate这样的公共接口结合使用,意味着提供对封装操作的访问。如果操作本身不是关键的话,那就没有问题。 - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接