Scala中trait内定义的case class无法序列化问题求解
定义在trait内部的case class属于路径依赖类型,通过DataStore.User构造的实例会隐式持有对外部封闭实例DataStore$(即DataStore单例对象)的$outer引用。Java原生序列化机制会递归序列化对象的所有非transient字段,包括这个隐式持有的外部引用,但DataStore单例未实现Serializable接口,因此抛出NotSerializableException,异常栈中提示的无法序列化的类tests.DataStore$就是这个单例对象的类型。
方案1:给继承Tables的单例混入Serializable(最小改动,优先推荐)
不需要改动自动生成的trait Tables代码,只需要在自行编写的、继承Tables的单例对象上混入Serializable特质即可:
package test import java.io.{ByteArrayOutputStream, ObjectOutputStream} trait Tables { case class User(id: Int, name: String) } // 仅修改这一行,给DataStore混入Serializable object DataStore extends Tables with Serializable { // 原有业务逻辑不需要任何改动 } object ScalaCacheTest { def serialize(value: Serializable): Array[Byte] = { val buf = new ByteArrayOutputStream() val os = new ObjectOutputStream(buf) os.writeObject(value) os.close() buf.toByteArray } def main(args: Array[String]) = { val user = DataStore.User(9527, "star") serialize(user) println("tests pass") } }
JDK原生对单例对象的序列化有特殊处理,反序列化时会保证返回唯一的单例实例,不会产生多余的对象副本,不会影响原有业务逻辑。
方案2:自定义序列化逻辑跳过不可序列化的$outer引用
如果不方便修改DataStore的继承关系,可以自定义ObjectOutputStream,在序列化时处理路径依赖类型的隐式外部引用,避免递归序列化不可序列化的对象:
import java.io.{ByteArrayOutputStream, ObjectOutputStream} import scala.util.Try def serialize(value: AnyRef): Array[Byte] = { val buf = new ByteArrayOutputStream() val os = new ObjectOutputStream(buf) { override def writeObject(obj: Any): Unit = { val targetObj = obj.asInstanceOf[AnyRef] // 查找路径依赖类型隐式持有的$outer字段 Try(targetObj.getClass.getDeclaredField("$outer")).toOption.foreach { field => field.setAccessible(true) val outerInstance = field.get(targetObj) // 如果外部引用对象不可序列化,将字段置为null避免序列化报错 if (outerInstance != null && !classOf[Serializable].isAssignableFrom(outerInstance.getClass)) { field.set(targetObj, null) } } super.writeObject(obj) } } os.writeObject(value) os.close() buf.toByteArray }
该方案需要注意反序列化时的兼容逻辑,避免$outer字段为null触发路径依赖类型校验错误,仅适合无法修改DataStore定义的场景。
方案3:替换为专业序列化框架(长期缓存场景推荐)
Java原生序列化本身性能差、序列化后体积大,并不适合做缓存序列化。可以替换为Kryo、uPickle、ScalaPB等专门的序列化框架,这类框架不会递归序列化路径依赖类型隐式持有的$outer引用,也不需要类实现Serializable接口,性能远高于Java原生序列化,且能天然规避这类问题。以Kryo为例,只需要提前注册对应的case class即可正常完成序列化/反序列化,不需要修改任何已有业务代码。
内容的提问来源于stack exchange,提问作者crowhite

