简体   繁体   English

使用惰性 val 序列化 case 类会导致 StackOverflow

[英]Serializing a case class with a lazy val causes a StackOverflow

Say I define the following case class:假设我定义了以下案例类:

case class C(i: Int) {
    lazy val incremented = copy(i = i + 1)
}

And then try to serialize it to json:然后尝试将其序列化为 json:

val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
val out = new StringWriter
mapper.writeValue(out, C(4))
val json = out.toString()
println("Json is: " + json)

It will throw the following exception:它将抛出以下异常:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]->C["incremented"]-
...

I don't know why is it trying to serialize the lazy val by default in the first place?我不知道为什么它首先尝试默认序列化惰性 val? This does not seem to me as the logical approach在我看来这不是合乎逻辑的方法

And can I disable this feature?我可以禁用此功能吗?

This happens because Jackson is designed for Java.发生这种情况是因为 Jackson 是为 Java 设计的。 Specifically, note that:具体来说,请注意:

  • Java has no idea of a lazy val Java 不知道lazy val
  • Java's normal semantics around fields and constructors don't allow the partitioning of fields into "needed for construction" and "derived for construction" (neither of those is a technical term) that Scala's combination of val in default constructor (implicitly present in a case class ) and val in a class's body provide Java 围绕字段和构造函数的正常语义不允许将字段划分为“构造所需”和“构造派生”(这两个都不是技术术语),Scala 在默认构造函数中的val组合(隐式存在于一个case class ) 和 class 主体中的val提供

The consequence of the second is that (except for beans, sometimes), Java-oriented serialization approaches tend to assume that anything which is a field (including private fields, since Java idiom is to make fields private by default) in the object needs to be serialized, with the ability to opt out through @transient annotations.第二个的结果是(有时除了 bean),面向 Java 的序列化方法倾向于假设对象中的任何字段(包括private字段,因为 Java 习惯用法是默认将字段设为private )需要被序列化,能够通过@transient注释选择退出。

The first, in turn, means that lazy val s are implemented by the compiler in a way that includes a private field.反过来,第一个意味着lazy val由编译器以包含private字段的方式实现。

Thus to a Java-oriented serializer like Jackson, a lazy val without a @transient annotation gets serialized.因此,对于像 Jackson 这样的面向 Java 的序列化程序,没有@transient注释的lazy val会被序列化。

Scala-oriented serialization approaches (eg circe, play-json, etc.) tend to serialize case class es by only serializing the constructor parameters.面向 Scala 的序列化方法(例如 circe、play-json 等)倾向于通过仅序列化构造函数参数来序列化case class

The solution I found was to use json4s for my serialization rather than jackson databind.我找到的解决方案是使用json4s进行序列化而不是 jackson 数据绑定。 My issue arose using akka cluster so I had to add a custom serlializer to my project.我的问题是使用 akka 集群出现的,所以我不得不在我的项目中添加一个自定义序列化程序。 For reference here is my complete implementation:作为参考,这里是我的完整实现:

class Json4sSerializer(system: ExtendedActorSystem) extends Serializer {

private val actorRefResolver = ActorRefResolver(system.toTyped)

object ActorRefSerializer extends CustomSerializer[ActorRef[_]](format => (
    {
        case JString(str) =>
            actorRefResolver.resolveActorRef[AnyRef](str)
    },
    {
        case actorRef: ActorRef[_] =>
            JString(actorRefResolver.toSerializationFormat(actorRef))
    }
))

implicit private val formats = DefaultFormats + ActorRefSerializer

def includeManifest: Boolean = true
def identifier = 1234567

def toBinary(obj: AnyRef): Array[Byte] = {
    write(obj).getBytes(StandardCharsets.UTF_8)
}

def fromBinary(bytes: Array[Byte], clazz: Option[Class[_]]): AnyRef = clazz match {
    case Some(cls) =>
        read[AnyRef](new String(bytes, StandardCharsets.UTF_8))(formats, ManifestFactory.classType(cls))
    case None =>
        throw new RuntimeException("Specified includeManifest but it was never passed")
}
}

You can't serialize that class because the value is infinitely recursive (hence the stack overflow).您无法序列化该类,因为该值是无限递归的(因此堆栈溢出)。 Specifically, the value of incremented for C(4) is an instance of C(5) .具体而言,的值incrementedC(4)是一个实例C(5) The value of incremented for C(5) is C(6) . C(5)incremented值为C(6) The value of incremented for C(6) is C(7) and so on... C(6)incremented值为C(7)等等...

Since an instance of C(n) contains an instance of C(n+1) it can never be fully serlialized.由于C(n)的实例包含C(n+1)的实例,因此它永远无法完全序列化。

If you don't want a field to appear in the JSON, make it a function:如果您不希望某个字段出现在 JSON 中,请将其设为函数:

case class C(i: Int) {
    def incremented = copy(i = i + 1)
}

The root of this problem is trying to serialise a class that also implements application logic, which breaches the principle of Separation of Concerns (The S in SOLID).这个问题的根源是试图序列化一个也实现应用程序逻辑的类,这违反了关注点分离原则(SOLID 中的 S)。

It is better to have distinct classes for serialisation and populate them from the application data as necessary.最好使用不同的类进行序列化,并根据需要从应用程序数据中填充它们。 This allows different forms of serialisation to be used without having to change the application logic.这允许使用不同形式的序列化,而无需更改应用程序逻辑。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM